Java工作笔记/Java面试题/Java八股文/Java常用API

码农工具包

hutool工具

hutool工具类判断各种类型数据

<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.8</version>
</dependency>
============================1.判断数组是否为空============================
String[] strArr = new String[]{};

// 判断数组不为null,且素组长度不为0
if (ArrayUtil.isNotEmpty(strArr)) {
    System.out.println("数组不为null,且素组长度不为0");
}

// 判断数组为null或数组长度为0
if (ArrayUtil.isEmpty(strArr)) {
    System.out.println("数组为null或数组长度为0");
}

============================2.判断集合是否为空============================
List<String> list = new ArrayList();

// 判断集合list是否为空,同时判断list为null,为空集合
if (CollectionUtil.isEmpty(list)) {
    System.out.println("集合list是否为空,同时判断list为null,为空集合");
}

// 判断集合list是否为空,同时判断list不为null,不为空集合
if (CollectionUtil.isNotEmpty(list)) {
    System.out.println("集合list是否为空,同时判断list不为null,不为空集合");
}

if (CollUtil.isNotEmpty(list)) {
    System.out.println("集合list是否为空,同时判断list不为null,不为空集合");
}

============================3.判断字符串是否为空============================
String str = null;

System.out.println("判断字符串是否为空:" + StrUtil.isNotBlank(str));
// 判断string不为"null"、""、" "
if (StrUtil.isNotBlank(str)) {
}

// 判断string为"null"、""、" "
if (StrUtil.isBlank(str)) {
}

============================4.判断两个字符串是否相等(内容相等)============================
String str1 = null;
String str2 = null;

System.out.println("判断两个字符串是否相等:" + ObjectUtil.equals(str1, str2));

// 判断两个字符串是否相等,此方法可以避免空指针异常
if (ObjectUtil.equals(str1, str2)) {
   /*  如果 string1 = null && string1 = null 返回true
     如果 string1 = null || string1 = null 返回false*/
}

hutool工具类时间字符串转换

============================获取当前时间============================
Date date = DateUtil.date(); //当前时间
Date date2 = DateUtil.date(Calendar.getInstance()); //当前时间
Date date3 = DateUtil.date(System.currentTimeMillis()); //当前时间
String now = DateUtil.now(); //当前时间字符串,格式:yyyy-MM-dd HH:mm:ss
String today= DateUtil.today();  //当前日期字符串,格式:yyyy-MM-dd

============================字符串转时间============================
eg1:
String dateStr = "2017-03-01";
Date date = DateUtil.parse(dateStr);
Date date = DateUtil.parse(dateStr, "yyyy-MM-dd");  //自定义日期格式转化

eg2:
String dateStr = "2015-08-12";
DateFormat format = new SimpleDateFormat("yyyy-MM-dd"); //自定义日期格式化的格式
Date classDate = format.parse(dateStr);//把字符串转化成指定格式的日期

============================时间转字符串============================
Date date = new Date();
String dateStr = DateUtil.format(date, "yyyy-MM-dd"); // 输出为(yyyy-MM-dd)格式的字符串

============================格式化日期输出============================
String dateStr = "2017-03-01";
Date date = DateUtil.parse(dateStr);

String format = DateUtil.format(date, "yyyy/MM/dd");  //结果 2017/03/01
String formatDate = DateUtil.formatDate(date);  //常用格式的格式化,结果:2017-03-01
String formatDateTime = DateUtil.formatDateTime(date);  //结果:2017-03-01 00:00:00
String formatTime = DateUtil.formatTime(date);  //结果:00:00:00

数据类型互相转换

Convert.toLong(str);
Convert.toInt(num);

开发日常编码

封装树结构

1.封装数据

主表

//value为标识编码,label为前端需要显示的数据
private String label;
private Integer value;
private List<DictData> children;

从表

private String label;
private Integer value;

接口

@GetMapping("/getDictList")
public AjaxResult getDictList(DictType type){
     List<DictType> list = deptService.selectArchiveTreeList(type);
     return AjaxResult.success(list);
}

@Override
public List<DictType> selectArchiveTreeList(DictType type) {
    return deptMapper.selectArchiveTreeList(type);
}

<resultMap id="dictListMap" type="com.sk.common.core.domain.entity.archivesEntity.DictType">
	<id column="dict_id" property="value"/>
	<result column="dict_name" property="label"/>
	<collection property="children" ofType="com.sk.common.core.domain.entity.archivesEntity.DictData">
		<result column="dict_code" property="value"/>
		<result column="dict_label" property="label"/>
	</collection>
</resultMap>
<select id="selectArchiveTreeList" resultMap="dictListMap">
	SELECT
		t.dict_id,
		t.dict_name,
		d.dict_label,
		d.dict_code
	FROM
		sys_dict_data AS d
			LEFT JOIN  sys_dict_type AS t ON t.dict_type = d.dict_type
	WHERE
		t.dict_type = 'sys_archives_type';
</select>

2.前端页面

<el-col :span="4" :xs="24">
  <div class="head-container">
    <el-tree
      :data="basicsOptions"
      :props="defaultProps"
      :expand-on-click-node="false"
      :filter-node-method="filterNode"
      ref="tree"
      default-expand-all
      highlight-current
      @node-click="handleNodeClick"
    />
  </div>
</el-col>

basicsOptions: undefined,
defaultProps: {
  children: "children",
  label: "label",
  value: "value"
},

// 筛选节点
filterNode(value, data) {
  if (!value) return true;
  return data.label.indexOf(value) !== -1;
},
// 节点单击事件
handleNodeClick(data) {
},
/** 查询部门下拉树结构 */
getBasicsTree() {
  basicsTreeSelect().then(response => {
    this.basicsOptions = response.data;
  });
}

在这里插入图片描述


通过时间范围查询数据

前端

<el-form-item label="上传时间">
  <el-date-picker
    v-model="form.queryTime"   <!-- queryTime: '' -->
    type="daterange"
    value-format="yyyy-MM-dd" <!--限制日期格式-->
    range-separator="至"
    start-placeholder="开始日期"
    end-placeholder="结束日期">
  </el-date-picker>
</el-form-item>
<el-form-item>
  <el-button type="primary" @click="query(form)">查询</el-button>
</el-form-item>

后台

@Override
public List<SkArchiveArchives> selectArchivesList(SkArchiveArchives archives) throws ParseException {
    //判断集合是否为空
 if (ArrayUtil.isNotEmpty(archives.getQueryTime())) {
    String[] queryTime = archives.getQueryTime();
    archives.setStartTime(queryTime[0]);
    String endTime = queryTime[1];
    DateFormat format = new SimpleDateFormat("yyyy-MM-dd"); //定义日期格式化的格式
    Date classDate = format.parse(endTime);//把字符串转化成指定格式的日期
    Calendar calendar = Calendar.getInstance(); //使用Calendar日历类对日期进行加减
    calendar.setTime(classDate);
    calendar.add(Calendar.DAY_OF_MONTH, 1);
    classDate = calendar.getTime();
    // 将日期类型转换成指定的字符串类型
    archives.setEndTime(DateUtil.format(classDate, "yyyy-MM-dd"));
}

在这里插入图片描述


java日期类型实现加减天数

String dateStr = "2015-08-12"; //需要加减的字符串型日期

DateFormat format = new SimpleDateFormat("yyyy-MM-dd"); //定义日期格式化的格式
Date classDate = format.parse(dateStr);//把字符串转化成指定格式的日期

Calendar calendar = Calendar.getInstance(); //使用Calendar日历类对日期进行加减
calendar.setTime(classDate);
calendar.add(Calendar.DAY_OF_MONTH, - 1);//-为昨天,+为明天

classDate = calendar.getTime();//获取加减以后的Date类型日期

dateSStr = DateUtil.format(classDate,"yyyy-MM-dd"); //得到处理以后的字符串

前端分页

 <!--分页-->
<pagination
  background
  v-show="total>0"
  :total="total"
  :page.sync="form.pageNum"
  :limit.sync="form.pageSize"
  @pagination="getList"
/>

/** 总条数 */
total: 0,

form: {
  pageNum: 1,
  /** 每页显示10条数据 */
  pageSize: 10,
},

//获取表单数据的方法
getList(){}

在这里插入图片描述

JSON

# 从redis取出token(token包含当前登录用户的信息)
JSONObject jsonObject = JSONObject.parseObject(String.valueOf(redisTemplate.opsForValue().get(token)));
String userName = jsonObject.getString("userName");

将对象转为json,目的使对象称为k,v格式

Data data = new Data();
JSONObject dataToJson = (JSONObject) JSONObject.toJSON(data);

注解

lombak相关注解

//生成所有字段的getter、toString()、hashCode()、equals()、所有非final字段的setter、构造器,相当于设置了 @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode
@Data

@Accessors(chain = true) //链式赋值
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private String name;
    private int score;
}
public class Main {
    public static void main(String[] args) {
        Student student = new Student().setName("yolo").setScore(98);//链式赋值
        String s = JSON.toJSONString(student);
        System.out.println(s);
    }
}

JSON相关注解、依赖

<!--以下两个依赖完成对象与json的相互转换-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>com.colobu</groupId>
    <artifactId>fastjson-jaxrs-json-provider</artifactId>
    <version>0.3.1</version>
</dependency>
public class Person implements Serializable {
    //ordinal:自定义字段的输出顺序  name:给字段起别名 serialize = false:表示不参与序列化
    @JSONField(ordinal = 1)
    private Integer eid;
    
    @JSONField(ordinal = 2,name = "user")
    private String username;
    
    @JSONField(serialize = false)
    private String sex;
    
    @JSONField(ordinal = 4)
    private Integer age;
}

表示不是数据库字段、时间格式化

@TableField(exist = false):表示该属性不为数据库表字段

@TableField(exist = true):表示该属性为数据库表字段。
    
/**
* date
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date date;
/**
* localDate
*/
@JsonFormat(pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate localDate;
/**
* localDateTime
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
/**
* localTime
*/
@JsonFormat(pattern = "HH:mm:ss")
private LocalTime localTime;

打包部署

1、上传jar包到服务器响应的文件目录下

2、停掉服务

ps -ef|grep sk-blo
kill -9 1111

3、备份当前jar包

mv 原jar包名 新名

4、修改上传的jar包名称

5、开启服务

nohup java -jar -Xms2g -Xmn1g java.jar >java.out 2>&1 &
java -jar ./java..jar

6、查看日志

tail -f -n 200 java.out

报错

杀进程

1、解决端口占用问题,首先查看端口的启动情况

win+R 输入cmd打开DOS命令框。输入:netstat -ano | findstr 80 其中80是我服务的端口号。

在这里插入图片描述

2、接下来我们就来杀死它

根据PID杀死任务: taskkill /F /PID “18560”

Git

idea查看git地址

查看git地址:git remote -v

git拉取代码提示Authentication failed for []

git拉取代码的时候提示Authentication failed for []

解决办法,用管理员身份打开git命令行,执行 git config --global credential.helper store重新clone的时候会提示让输入用户名,然后弹出框让输入密码,就可以了

跑项目时报错


Cannot resolve com.sun:tools:1.8解决方法

引入阿里的**druid **启动器,druid-spring-boot-starter-1.2.6.pom

<!--数据库连接池-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.6</version>
</dependency>

启动器应用的druid,druid-1.2.6.pom

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.6</version>
</dependency>

重点是里面的profiles,会根据系统选择。这里只选择windows的

<profile>
	<id>jconsole</id>
	<activation>
		<!--<activeByDefault>true</activeByDefault>-->
		<file>
			<exists>${env.JAVA_HOME}/lib/jconsole.jar</exists>
		</file>
	</activation>
    <properties>
	<toolsjar>${env.JAVA_HOME}/lib/tools.jar</toolsjar>
	<jconsolejar>${env.JAVA_HOME}/lib/jconsole.jar</jconsolejar>
</properties>
...
</profile>

根据JAVA_HOME获取jar包,所以要看一下环境配置是否正确。

最后解决办法,原因是因为maven版本3.8.2有问题,没有搜索到jar包,换成3.6.3的就解决了

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

报的警告,但是过了,程序员无视警告就好了

在这里插入图片描述


java: You aren’t using a compiler supported by lombok, so lombok will not work and has been disab…

报错问题

java: You`aren't using a compiler supported by lombok, so lombok will not work and has been disabled.
  Your processor is: com.sun.proxy.$Proxy26
  Lombok supports: sun/apple javac 1.6, ECJ

解决

方法一

在以下位置加上该配置

-Djps.track.ap.dependencies=false

img

方法二

更新一下版本到以下版本

<!--Lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.14</version>
    <scope>provided</scope>
</dependency>

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.kimi.service.edu.mapper.EduCourseMapper.getPublishCourseInfo

因为maven默认加载机制,它只会把src-main-java文件夹中的java类型文件进行加载,其它类型文件不会加载。

在target文件夹中也没有加载xml文件夹中的xml文件

解决方式:

1.将xml文件复制到target文件夹中的mapper文件夹里面去;

2.将xml文件写到resources文件下;

3.通过配置文件进行配置,让maven自动加载xml文件:

pom.xml文件中添加如下配置:

<build>
<resources>
    <resource>
        <directory>src/main/java</directory>
        <includes>
            <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
    </resource>
</resources>
</build>

application.properties文件中添加如下配置:

# 配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/kimi/service/edu/mapper/xml/*xml

com.mongodb.MongoSocketOpenException: Exception opening socket

23:29:11.057 [cluster-ClusterId{value='6318b8c44a60ed674eff44ac', description='null'}-localhost:27017] INFO  o.m.driver.cluster - [info,76] - Exception in monitor thread while connecting to server localhost:27017
com.mongodb.MongoSocketOpenException: Exception opening socket
 at com.mongodb.internal.connection.SocketStream.open(SocketStream.java:70)
 at com.mongodb.internal.connection.InternalStreamConnection.open(InternalStreamConnection.java:143)
 at com.mongodb.internal.connection.DefaultServerMonitor$ServerMonitorRunnable.lookupServerDescription(DefaultServerMonitor.java:188)
 at com.mongodb.internal.connection.DefaultServerMonitor$ServerMonitorRunnable.run(DefaultServerMonitor.java:144)
 at java.lang.Thread.run(Thread.java:745)
Caused by: java.net.ConnectException: Connection refused: connect
 at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
 at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:85)
 at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
 at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
 at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
 at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
 at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
 at java.net.Socket.connect(Socket.java:589)
 at com.mongodb.internal.connection.SocketStreamHelper.initialize(SocketStreamHelper.java:107)
 at com.mongodb.internal.connection.SocketStream.initializeSocket(SocketStream.java:79)
 at com.mongodb.internal.connection.SocketStream.open(SocketStream.java:65)
 ... 4 common frames omitted

解决方法

springboot自动配置了支持mongodb。在启动springboot时会自动实例化一个mongo实例,需要禁用自动配置 ,
    
增加@SpringBootApplication(exclude = MongoAutoConfiguration.class)这个注解即可		

Java基础


八大基本类型

在这里插入图片描述

JDK1.8新特性

JDK1.7和JDK1.8的区别

Java8 是 Java发布以来改动最大的一个版本
添加了函数式编程、Stream、全新的日期处理类
函数式编程新加了一些概念:Lambda表达式、函数式接口、函数引用、默认方法、Optional类等
Stream中提供了一些流式处理集合的方法,并提供了一些归约、划分等类的方法
日期中添加了ZoneDateTime、DataFormat等线程安全的方法类

1.JDK1.8接口中除了定义抽象方法以外,还可以定义static和default方法, static和default方法有方法体;

2.JDK1.8中switch语句支持String;

扩展:

基本类型有:byte,short,int,char

包装类型有:Byte,Short,Integer,Character,String,enum

switch实际上只支持int类型,使用其他的类型时是通过转化支持的:

1、基本类型byte char short ,原因:这些基本数字类型可自动向上转为int, 实际还是用的int。

2、包装类型Byte,Short,Character,Integer ,原因:java的自动拆箱机制 可将这些对象自动转为基本类型

3、String 类型 原因:实际switch比较的string.hashCode值,它是一个int类型

4、enum类型 原因 :实际比较的是enum的ordinal值(表示枚举值的顺序),它也是一个int类型

3.Lamdba表达式

4.函数式接口

5.日期中添加了ZoneDateTime、DataFormat等线程安全的方法类

因为JDK1.8新特性比较多,只记得这几个!


final, finally, finalize 的区别

final

final修饰变量时,如果是基本类型的变量,在声明时必须给定初始值,则其数值一旦被初始化后就不能被修改。如果是引用类型的变量,则初始化之后便不能再让其指向另外一个对象,但是它指向对象的内容可以被改变。

final修饰 的方法不能被重写,但是可以重载。

final修饰的类称为终类,该类不能被继承,final不能和abstract一起使用。

finally

finally用在try/catch/finally语句中,表示这段代码最终会被执行,通常被用来释放资源、关闭资源等操作。但是有时候finally语句不会执行,比如当程序执行try语句时,所有线程被结束,finally语句就不会执行。

try{
    System.out.println("没有异常时执行");
    System.exit(0);//正常退出   System.exit(0); 会结束整个虚拟机,也就是所有线程
   // System.exit(-1);
}catch (Exception e){
    System.out.println("发生异常时执行");
}finally {
    System.out.println("最终会被执行");
}

延伸:try块中的内容是在无异常时执行到结束。catch块中的内容,是在try块内容发生catch所声明的异常时,

跳转到catch块中执行。

finalize

finalize是Object类中的一个方法,在GC执行的时候会被调用回收对象的finalize()方法,可以覆盖此方法来实现对其它资源的回收,例如关闭文件等。需要注意的是,一旦GC准备好释放对象的占用空间,将首先调用其finalize()方法,并且在下一次GC回收动作发生时,才会真正回收对象占用的内存。

由于finalize()方法存在很多问题,JDK1.9之后已经弃用了该方法。


栈、堆、方法区

栈:基本数据类型、堆中对象的引用、局部变量

堆:对象、数组、静态变量、字符串常量池

方法区:类信息、运行时常量池


多态

父类引用指向子类对象,同一类的对象收到相同消息时,会得到不同的结果。而这个消息是不可预测的。多态,顾名思义,就是多种状态,也就是多种结果。

反射

运行时拿到类的字节码对象(.class),得到类中的属性和方法。

这种动态获取的信息以及动态调用对象方法的功能称为java语言的反射机制。

项目中Spring框架IOC就使用了反射机制。


notify和notifyAll的区别

notify()和notifyAll()都是用来唤醒调用wait()方法进入等待锁资源队列的线程,区别在于:

notify():唤醒正在等待此对象监视器的单个线程。如果有多个线程在等待,则选择其中一个随机唤醒(由调度器决定),唤醒的线程享有公平竞争资源的权利。

notifyAll():唤醒正在等待此对象监视器的所有线程,唤醒的所有线程公平竞争。

sleep和wait的区别

1.sleep是线程类Tread的静态方法,wait是Object类的方法;

2.sleep是使线程休眠,不会释放对象锁;wait是使线程等待,释放锁;

sleep让出的是cpu,如果此时代码是加锁的,那么即使让出了CPU,其他线程也无法运行,因为没有得到锁;
wait是让自己暂时等待,放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)
后本线程才进入对象锁定池准备获得对象锁进入运行状态。

3.调用sleep使线程进入阻塞状态;调用 wait使线程进入就绪状态。

悲观锁和乐观锁

悲观锁:
当多事务/多线程并发执行时,事务总是悲观的认为,在自己访问数据期间,其他事务一定会并发执行,此时会产生线程安全问题,
所以为了保证线程安全,这个事务在访问数据时,立即给数据加锁,从而保证线程安全.
特点:可以保证线程安全,但是并发执行效率低下    
synchronized、排它锁都是悲观锁的应用
 
乐观锁:
在多线程/多事务并发执行中,某个事务总是乐观的认为,在自己执行期间,没有其他事务与之并发,认为不会产生线程安全问题,所以不会给数据加锁;但是确实存在其他事务与之并发执行的情况,确实存在线程安全问题,为了保证线程安全,通过版本号机制或CAS来保证线程安全.  
CAS:compare and swap  比较并交换

线程篇

线程创建的四种方式,什么地方使用了线程池

创建线程的方式:1.继承Thread类

​ 2.实现Runnable接口

​ 3.实现callable接口

CallableRunnable的区别:1.Callable重写的是call()方法,Runnable重写的是run()方法
						2.实现Callable接口有返回值,实现Runnable接口没有返回值

​ 4.线程池创建线程

平时使用那种线程池:

提高一下插入表的性能优化,因为是两张表,先插旧的表,紧接着插新的表,一万多条数据就有点慢了

后面就想到了线程池ThreadPoolExecutor,而用的是Spring Boot项目,

可以用Spring提供的对ThreadPoolExecutor封装的线程池ThreadPoolTaskExecutor,直接使用注解启用

使用步骤

先创建一个线程池的配置,让Spring Boot加载,用来定义如何创建一个ThreadPoolTaskExecutor,要使用@Configuration和@EnableAsync**(/ əˈsɪŋk /)**这两个注解,表示这是个配置类,并且是线程池的配置类;

创建一个Service接口,是异步线程的接口;

实现类;

将Service层的服务异步化,在executeAsync()方法上增加注解@Async(“asyncServiceExecutor”),asyncServiceExecutor方法是前面ExecutorConfig.java中的方法名,表明executeAsync方法进入的线程池是asyncServiceExecutor方法创建的。

在Controller里或者是哪里通过注解@Autowired注入这个Service

线程池

为什么要使用线程池

创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。使用线程池可以把创建和销毁的线程的过程去掉。

线程池有什么作用

1、提高效率 创建好一定数量的线程放在池中,等需要使用的时候就从池中拿一个,这要比需要的时候创建一个线程对象要快的多。
2、方便管理 可以编写线程池管理代码对池中的线程同一进行管理,比如说启动时有该程序创建100个线程,每当有请求的时候,就 分配一个线程去工作,如果刚好并发有101个请求,那多出的这一个请求可以排队等候,避免因无休止的创建线程导致系统崩溃。

**线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险。 **

四种常见线程池

1.newFixedThreadPool

创建一个固定大小线程池,可控制线程最大并发数,超出的线程会在队列中等待。

固定大小的线程池,可以指定线程池的大小,该线程池corePoolSize和maximumPoolSize相等,阻塞队列使用的是LinkedBlockingQueue,大小为整数最大值。

该线程池中的线程数量始终不变,当有新任务提交时,线程池中有空闲线程则会立即执行,如果没有,则会暂存到阻塞队列。对于固定大小的线程池,不存在线程数量的变化。同时使用无界的LinkedBlockingQueue来存放执行的任务。当任务提交十分频繁的时候,LinkedBlockingQueue迅速增大,存在着耗尽系统资源的问题。而且在线程池空闲时,即线程池中没有可运行任务时,它也不会释放工作线程,还会占用一定的系统资源,需要shutdown。

2.newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

单个线程线程池,只有一个线程的线程池,阻塞队列使用的是LinkedBlockingQueue,若有多余的任务提交到线程池中,则会被暂存到阻塞队列,待空闲时再去执行。按照先入先出的顺序执行任务。

3.newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

缓存线程池,缓存的线程默认存活60秒。执行效率最快,线程的核心池corePoolSize大小为0,核心池最大为Integer.MAX_VALUE,阻塞队列使用的是SynchronousQueue。是一个直接提交的阻塞队列, 他总会迫使线程池增加新的线程去执行新的任务。在没有任务执行时,当线程的空闲时间超过keepAliveTime(60秒),则工作线程将会终止被回收,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销。如果同时又大量任务被提交,而且任务执行的时间不是特别快,那么线程池便会新增出等量的线程池处理任务,这很可能会很快耗尽系统的资源。

4.newScheduledThreadPool(/ ˈskedʒuːld )

创建一个定长线程池,支持定时及周期性任务执行

定时线程池,该线程池可用于周期性地去执行任务,通常用于周期性的同步数据。

scheduleAtFixedRate:是以固定的频率去执行任务,周期是指每次执行任务成功执行之间的间隔。

schedultWithFixedDelay:是以固定的延时去执行任务,延时是指上一次执行成功之后和下一次开始执行的之前的时间。

线程池七大参数
public ThreadPoolExecutor(int corePoolSize, //线程池中常驻核心线程数
                          int maximumPoolSize, //线程池能够容纳同时执行的最大线程数,此值必须大于1
                          
						//多余空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余空闲线程会被销毁直到剩下corePoolSize为止。
                          long keepAliveTime, 
                          TimeUnit unit, //keepAliveTime的单位
                          BlockingQueue<Runnable> workQueue,//:里面放了被提交但是尚未执行的任务
                          ThreadFactory threadFactory, //表示线程池中工作线程的线程工厂,用于创建线程
                          
                     //拒绝策略,当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,对任务的拒绝方式。
                          RejectedExecutionHandler handler) 

synchronized(同步锁)

synchronized可以修饰静态方法成员函数,同时还可以直接定义代码块

但是归根结底它上锁的资源只有两类:一个是对象,一个是

Synchronized如何避免死锁
Synchronized不要嵌套定义;
synchronized底层实现原理

synchronized具有四个特性:原子性,可见性,有序性,可重入性(可重入锁)

synchronized是可重入锁,每次锁对象会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。

注意!面试时经常会问比较synchronized和volatile(可见性),它们俩特性上最大的区别就在于原子性,volatile不具备原子性。

volatile只能作用于变量,保证了操作可见性和有序性,不保证原子性。


多线程怎么解决线程安全问题

1.用局部变量去取代实例变量和静态变量。
2.如果是实例变量,则可以创建多个实例。
3.用Synchronized加锁。

Java多线程之间的通讯和协作

生产者-消费者模型

可以通过中断和共享变量的方式实现线程间的通讯和协作;

比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,消费者通知生产者队列空间有了。同样地,当队列空时,消费者也必须等待,等待生产者通知消费者队列中有商品了。这种互相通信的过程就是先线程间的协作。

Java中线程通信协作的最常见三种方式:
1.Syncrhoized加锁线程的Object类中的wait()/notify()/notifyAll();
 wait:等待
 让作用在仓库上的线程处于等待状态,会释放锁
 notify: 唤醒
可以唤醒仓库上等待的线程
 wait和notify方法是object方法,sleep是Thread方法
 
2.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
线程间直接的数据交换:
通过管道进行线程间通信:1)字节流、2)字符流

3.volatile(可见性)

代码解析

public class No4 {

    public static void main(String[] args) {
        List list = new ArrayList<>();
        Thread t1 = new Thread(new Producer(list));
        Thread t2 = new Thread(new Consumer(list));
        t1.setName("生产者");
        t2.setName("消费者");
        t1.start();
        t2.start();
    }
}

class Producer implements Runnable {
    List list;
    public Producer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        while (true) {
            synchronized (list) {
                if (list.size() > 0) {
                    try {
                        list.wait();
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //说明仓库为空,可以生产
                Object producer = new Object();
                list.add(·);
                System.out.println(Thread.currentThread().getName() + "===>" + producer);
                list.notify();
            }
        }
    }
}

class Consumer implements Runnable {
    List list;
    public Consumer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        while (true) {
            synchronized (list) {
                if (list.size() == 0) {
                    try {
                        list.wait();
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //说明仓库里可以被消费
                Object remove = list.remove(0);
                System.out.println(Thread.currentThread().getName() + "===>" + remove);
                list.notify();
            }
        }
    }
}

线程三大特性

/**
 * 线程有三个特性:可见性(volatile)、原子性、安全性
 */
    static volatile boolean a = true;//volatile保证多个线程之间可见性,必须至少有两个线程一写一读

    public static void main(String[] args) throws InterruptedException {
        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        while (a) {
//                            try {
//                                Thread.sleep(1000);
//                            } catch (InterruptedException e) {
//                                e.printStackTrace();
//                            }
                        }
                    }
                }
        ).start();
        Thread.sleep(1000);
        a = false; //写入
        System.out.println(a); //读取
    }
}

    //static int a = 0;
    static AtomicInteger a = new AtomicInteger(0);//AtomicInteger(/ əˈtɒmɪk /):原子性

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
//                    synchronized (Demo06.class){
//                        a++;
//                    }

                    a.getAndAdd(1);

                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(a);
    }

线程的5种状态

1、新建状态(New):新创建了一个线程对象。

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。

**3、运行状态(Running):**就绪状态的线程获取了CPU,执行程序代码。

**4、阻塞状态(Blocked):**阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

阻塞的情况分三种:

(1)、等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“**等待池”**中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,

(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入**“锁池”**中。

(3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

**5、死亡状态(Dead):**线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

start()和run()的区别

new一个Thread,线程进入新建状态,调用start()方法,使线程进入就绪状态,当得到cpu时间片后自动调用run(),run()执行结束后才能继续执行下一个run(),所有run()并没有多线程的体现。

start()
用 start方法来启动线程,是真正实现了多线程, 通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法。但要注意的是,此时无需等待run()方法执行完毕,即可继续执行下面的代码。所以run()方法并没有实现多线程。

run()
run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码。

区别:
1、线程中的start()方法和run()方法的主要区别在于,当程序调用start()方法,将会创建一个新线程去执行run()方法中的代码。但是如果直接调用run()方法的话,会直接在当前线程中执行run()中的代码,注意,这里不会创建新线程。这样run()就像一个普通方法一样。

2、另外当一个线程启动之后,不能重复调用start(),否则会报IllegalStateException异常。但是可以重复调用run()方法。

总结起来就是run()就是一个普通的方法,而start()会创建一个新线程去执行run()的代码。

还有:
1、start方法用来启动相应的线程;

2、run方法只是thread的一个普通方法,在主线程里执行;

3、需要并行处理的代码放在run方法中,start方法启动线程后自动调用run方法;

4、run方法必去是public的访问权限,返回类型为void。

如果直接调用线程类的run()方法,这会被当做一个普通的函数调用,程序中仍然只有主线程这一个线程,也就是说,start()方法能够异步地调用run()方法,但是直接调用run()方法却是同步的,因此也就无法达到多线程的目的。

只有通过调用线程类的start()方法才能真正达到多线程的目的。


过滤器和拦截器的区别?

1.拦截器(Interceptor)是基于Java的反射机制(属于面向切面编程(AOP)的一种运用),而过滤器(Filter)是基于函数回调。
2.过滤器依赖于servlet容器,而拦截器不依赖于servlet容器。
3.过滤器可以对几乎所有的请求起作用,拦截器只能对action请求起作用。
4.过滤器不能获取IOC容器中的各个bean,而拦截器可以,在拦截器里注入一个service,可以调用业务逻辑。

过滤器(Filter):

它依赖于servlet容器,它可以对几乎所有请求进行过滤。使用过滤器的目的,是用来做一些过滤操作,获取我们想要获取的数据,

比如:在Javaweb中,对传入的request、response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或controller进行业务逻辑操作。

通常用的场景是:在过滤器中修改字符编码(CharacterEncodingFilter)、在过滤器中修改HttpServletRequest的一些参数(XSSFilter[自定义过滤器]),如:过滤低俗文字、危险字符等。

拦截器(Interceptor):

它依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。在实现上,基于Java反射机制。

属于面向切面编程(AOP)的一种运用,就是在servlet或一个方法前,调用一个方法,或者在方法后,调用一个方法,比如动态代理就是拦截器的简单实现,在调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在调用方法后打印出字符串,甚至在抛出异常的时候做业务逻辑操作。

由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。拦截器可以对静态资源的请求进行拦截处理。

两者的本质区别:

拦截器(Interceptor)是基于Java的反射机制,而过滤器(Filter)是基于函数回调。

从灵活性上说拦截器功能更强大些,Filter能做的事情,都能做,而且可以在请求前,请求后执行,比较灵活。

Filter主要是针对URL地址做一个编码的事情、过滤掉没用的参数、安全校验(比较泛的,比如登录不登录之类),太细的话,还是建议用interceptor。不过还是根据不同情况选择合适的。


迭代器

Iterable接口称为迭代器,主要用于遍历实现Collection接口的集合中的元素。

特点是更加安全,因为它可以确保在当前遍历的集合元素被修改的时候会抛出

ConcurrentModificationException异常

Iterator怎么使用,有什么特点:

1.java.lang.Iterable接口被java.util.Collection接口继承,java.util.Collection接口的iterator()方法返回一个Iterator对象;

2.next()方法获得集合的下一个元素;

3.hasNext()检查集合是否还存在下一个元素;

4.remove()方法将迭代器返回的元素删除。


集合篇

Java集合框架继承图

img


那些集合是线程安全的

  • Vector:就比ArrayList多了个同步化机制(线程安全)
  • Stack:栈,也是线程安全,继承与Vector
  • HashTable:就比HashMap多了个线程安全
  • ConcurrentHashMap:是一种高效但是线程安全的集合

综合面试题

1.List、Set、Queue、Map四者的区别
  • List、Set、Queue继承自Collection
  • List存储的元素是有序的、可重复的
  • Set存储的元素是无序的、不可重复的
  • Queue按照特定的排队规则来排序,存储的元素是有序的、可重复的
  • Map使用Key-value来进行存储数据,key不可重复、value是无序的、可重复的
2.集合的底层数据结构

List

  • 实际上有两种List:一种是基本的ArrayList,其优点在于随机访问元素,另一种是LinkedList,它并不是为快速随机访问设计的,而是快速的插入或删除。

    • ArrayList:底层由**Object[]**数组实现。允许对元素进行快速随机访问,但是向List中间插入与移除元素的速度很慢。

    • LinkedList :底层是链表结构。对顺序访问进行了优化,向List中间插入与删除的开销并不大。随机访问则相对较慢。还具有下列方 法:addFirst(), addLast(), getFirst(), getLast(), removeFirst() 和 removeLast(),

      这些方法 (没有在任何接口或基类中定义过)使得LinkedList可以当作堆栈、队列和双向队列使用。

Set

  • 简单回答:set集合底层就是一个Map集合,区别是Set是一个单列数据,也就是一个一个的数据,Map是双列数据,就是以键值对的形式存储数据。

  • 复杂回答:Set集合的底层就是数组+分支的结构,简称Hash表(散列表)。

  • 往Set集合中添加元素的算法:

    根据对象的地址算出一个hasCode值,如果底层数组中没有对应的hashCode值,则找一个数组空间放进去,如果集合中有了对应的hashCode值对象,则进一步判断两个对象是否为equlas,如果不为equlas,则新加的对象以链表的形式挂在对应的hashCode的值元素下,如果两个对象equlas,则认为对象重复了,就不添加。所以往set集合中添加的元素会

    重写hashCode方法和equlas方法。

    • HashSet(无序、唯一):底层采用HasMap来保存元素。为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。

    • TreeSet(有序、唯一): 底层基于TreeMap实现,TreeMap底层是红黑树。保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列,TreeSet集合中存放的对象必须实现了comparable接口,因为红黑树要求TreeSet集合元素

      必须按照顺序存放。

Queue

  • ArrayDeque:Object[]数组+双指针
  • PriorityQueue:Object[]数组来实现二叉堆

Map

  • HashMap jdk1.8之前由数组+链表组成,数组是HashMap的主体,而链表主要是为了解决哈希冲突。jdk1.8之后引入了红黑树。当链表长度大于等于8且数组长度大于等于64,链表为转换为红黑树。如果只满足一个条件,那么会优先选择数组扩容。
  • TreeSet底层基于TreeMap实现
  • TreeMap:红黑树(自平衡二叉排序树)
  • HashTable:数组+链表组成,数组是HashTable的主体,链表则是主要为了解决哈希冲突而存在的。
3.如何选取集合结构
  • 如果我们需要根据键值来获取元素值,就可以选用Map接口下的集合。需要排序就选用TreeMap,不需要排序就选用HashMap,需要保证线程安全就选用ConcurrentHashMap(Concurrent:/ kənˈkʌrənt /);
  • 如果只需要保存元素值,就选择实现Conllection接口的集合。需要保证元素唯一时选择Set接口下的集合,比如TreeSet或者是HashSet,不需要就选用List接口下的集合,比如ArrayList或者LinkedList
4.为什么要使用集合

当需要保存一组数据类型相同的数据的时候,由于数据的类型是多种多样的,所以我们使用了集合。集合提高了数据存储的灵活性,还可以保存具有映射关系的数据。

List

1、List常用方法
.add():添加元素			.remove(index):通过下标删除元素		.remove(Object o):删除指定的一个元素
.contains(Object o):是否包含某个元素					   .set(index,element):将index位置的元素替换为element 
.add(index,element):将element元素放到index位置,原来的元素后移一位
.indexOf(Object o):显示集合中元素第一次出现的下标	           .lastIndexOf(Object o):显示集合中元素最后一次出现的下标
.size():得到集合中元素的个数		.subList(fromIndex,toIndex):截取集合中的元素生成一个新集合(含头不含尾)
.equals():判断两个集合是否相等	.isEmpty():判断集合是否为空		.toString():集合转为字符串
    
//将多个对象同时添加到一个集合中    
Article article1 = new Article();
Article article2 = new Article();
Article article3 = new Article();
ArrayList<Article> list = new ArrayList<>();
Collections.addAll(list,article1,article2,article3);

   
// 迭代器
.iterator():返回Iterator集合对象
Iterator<String> iterator = person.iterator();
while (iterator.hasNext()){
    System.out.println(iterator.next());
}

.toArray():集合转为数组
String[] phones = (String[]) phone.toArray();
System.out.println("将集合转换为数组:"+phones);
2.ArrayList与LinkedList的区别?

(1)是否保证线程安全: ArrayList和LinkedList都是不同步的、线程不安全的。

(2)底层实现: ArrayList底层使用Object[]数组实现,LinkedList底层使用双向链表实现。(jdk1.6之前是双向循环链表,1.6之后取消了循环)。

(3)是否支持快速随机访问: ArrayList底层使用数组实现,支持快速访问。而LinkedList底层是链表实现,不支持快速访问。

(4)空间内存占用:ArrayList的空间浪费主要体现在list列表结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素存储都需要比ArrayList消耗更多的空间(因为节点要放置前驱和后继)。

3.System.arraycopy()方法和Arrays.copyOf()方法的区别

通过查看源码我们可以知道,Arrays.copyOf()方法内部调用了System.arraycopy()方法。

arraycopy()方法需要目标数组,将原数组拷贝到自定义的数组中或者是原数组,并且可以选择拷贝的起点以及放入新的数组中的位置。

copyOf()时系统自动在内部新建一个数组,并返回该数组。

拓展:

ArrayList实现了RandomAccess接口,只是作为一个标识,说明ArrayList支持快速随机访问功能。

4.ensureCapacity()方法的作用

ensureCapacity()方法并不是在ArrayList内部调用的,而是提供给用户来使用的,在向ArrayList里面添加大量元素之前最好先使用ensureCapacity方法,以减少增量重新分配的次数,提高效率。

5.ArrayList扩容机制
  • 先创建的ArrayList的容量为0
  • 第一次调用add()方法扩容成10
  • 当容量不够需要扩容时按照1.5倍的速度进行扩容
6.ArrayList如何做到并发修改,而不出现并发修改异常?

为解决此问题呢,java引入了一个可以保证读和写都是线程安全的集合(读写分离集合):CopyOnWriteArrayList

所以解决方案就是:

// private static ArrayList<String> list = new ArrayList<>();
 // 使用读写分离集合替换掉原来的ArrayList
 private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
 static {
     list.add("Jack");
     list.add("Amy");
     list.add("Lucy");
 }
7.ArrayList和Vector的区别?
  • ArrayList是List的主要实现类,底层使用Object[]数组存储,线程不安全;
  • Vector是List的古老实现类,底层使用Object[]存储,线程安全。

Set

1.comparable 和 Comparator 的区别
  • comparable接口实际上是出自java.lang包中的一个CompareTo(Object obj)方法用来排序;
  • comparator实际上是出自java.util包中有一个compare(Object obj1,Object obj2)方法来排序。

当我们需要对一个集合进行自定义排序后,我们就要重写CompareTo或者是compare方法。

2.无序性和不可重复性的含义

(1)无序性?无序性不等于随机性,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。

(2)不可重复性?不可重复性是指添加的元素按照equals判断时,返回false,需要同时重写equals方法和hashCode方法。

3.比较HashSet、LinkedListHashSet、TreeSet的异同
  • HashSet、LinkedListHashSet、TreeSet三者都是Set集合的实现类,都能保证元素唯一,并且都不是线程安全的。
  • HashSet、LinkedListHashSet、TreeSet的主要区别在于底层数据结构不同。HashSet底层数据结构式HashMap(哈希表);LinkedListHashSet的底层数据结构是链表和哈希表。TreeSet的底层数据结构是红黑树,元素是有序的。
  • HashSet用于不需要保证元素插入和去除顺序的场景;LinkedListHashSet用于保证元素的插入和取出满足FIFO的场景;TreeSet用于支持元素自定义排序规则的场景。

Queue

1.Queue与Deque的区别

Queue是单端队列,只能从一端插入元素,另一端删除元素,遵循FIFO的规则。

Queue根据容量问题而导致操作失败后的处理方式不同。一种抛出异常,一种返回null。

Deque是双端队列,在队列的两端均可以插入或者删除元素。

实际上,Deque还提供了其他的方法,可以当作栈来使用。

2.ArrayDeque和LinkedList的区别

ArrayDeque和LinkedList都实现了Deque接口,两者都可以当作队列来使用。

  • ArrayDeque是基于可变容量的数组和双指针来实现的,而LinkedList是通过链表来实现的
  • ArrayDeque不支持Null数据,而LinkedLisr可以使用Null数据
  • ArrayDeque是在jdk1.6引入的,而LinkedList早在jdk1.2都已存在
  • ArrayDeque插入时可能存在扩容过程,不过插入的操作时间复杂度仍为O(1),虽然LinkedList不需要扩容,但是每次插入数据时都需要重新申请空间,性能相比更差一点

从性能上来说,ArrayDeque来实现队列要比LinkedList要好一点,另外,ArrayDeque也可以直接用作栈。

3.简单说一下PriorityQueue

PriorityQueue是在jdk1.5被引入的,与Queue的区别在于元素出队顺序是与优先级相关的,即总是优先级高的元素出队列。

  • PriorityQueue利用了二叉堆的数据结构来实现,底层使用可变长的数组来存储数据
  • PriorityQueue通过堆元素的上浮和下沉,实现了在(logn)的时间复杂度内插入元素和删除堆顶元素
  • PriorityQueue是非线程安全的,而且不支持存储NULL对象
  • PriorityQueue默认是小顶堆,但是可以接收一个comparator作为构造参数,可以自定义元素优先级的先后

Map

1.HashMap和Hashtable的区别
  • 线程是否安全:HashMap是非线程安全的,而Hashtable是线程安全的。Hashtable内部的方法基本都经过synchronized修饰。

  • 效率:因为线程安全的问题,HashMap要比Hashtable效率高,而且Hashtable已经快要淘汰了。HashMap线程不安全,HashTable线程安全,要使用线程安全的map集合可以使用ConcurrentHashMap;

  • 初始容量和每次扩充容量的大小:

    • 创建时如果不指定初始容量,Hashtable默认初始容量为11,每次扩充之后,边缘原来的 2n+1 ;而HashMap默认初始化大小为16,每次扩容会变为原来的2倍。
  • Hashmap是允许key和value为null值的,用containsValue和containsKey方法判断是否包含对应键值对;

    ​ HashTable键值对都不能为空,否则包空指针异常。

  • 底层数据结构:JDK1.8之后HashMap在解决哈希冲突时,当链表长度大于8且数组长度大于64,链表就会转化为红黑树,若有一个条件不满足,那么会优先选择数组扩容。而Hashtable没有这样的机制。

2.HashMap和HashSet的区别

HashSet底层就是基于HashMap实现的。

HashMap实现了Map接口,而HashSet实现了Set接口。

HashMap用于存储键值对,而HashSet用于存储对象。

HashMap不允许有重复的键,可以允许有重复的值。HashSet不允许有重复元素。

HashMap允许有一个键为空,多个值为空,HashSet允许有一个空值。

HashMap中使用put()将元素加入map中,而HashSet使用add()将元素放入set中。

HashMap比较快,因为其使用唯一的键来获取对象。

3.HashMap和TreeMap的区别

HashMap和TreeMap都继承于AbstractMap,但是TreeMap还实现了NavigableMap接口和SortedMap接口。

  • 实现Navigable接口让TreeMap对集合内元素有了搜索的功能;
  • 实现SortedMap接口让TreeMap有了对集合中的元素根据键排序的能力。

相比来说,TreeMap多了对集合中元素根据键排序的功能和对集合元素进行搜索的能力。

4.HashSet如何检查重复?

当把对象加入到HashSet中时,HashSet会先计算对象的hashCode值来判断对象加入的下标位置,同时也会与其他的对象的hashCode进行比较,如果没有相同的,就直接插入数据;如果有相同的,就进一步使用equals来进行比较对象是否相同,如果相同,就不会加入成功。5.HashCode与equals的相关规定

(1)如果两个对象相等,则hashCode也一定是相等的

(2)两个对象相等,对两个进行equals也会返回true

(3)两个对象有相同的hashCode,他们也不一定是相等的

(4)hashCode和equals方法都必须被同时覆盖

6.HashMap的长度为什么是2的n次方?

为了能让HashMap存取高效,尽量减少哈希碰撞,尽量把数据均匀分配。

7.HashMap多线程下操作导致死循环问题

主要原因是并发下的Rehash会造成元素之间形成一个循环链表。多线程下HashMap会存在数据丢失的问题,并发环境下推荐使用ConcurrentHashMap、

8.ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashmap和Hashtable的区别主要体现在实现线程安全的方式上不同

  • 底层数据结构: jdk1.7的ConcurrentHashMap底层采用分段的数组+链表实现,jdk1.8采用的数据结构跟HashMap1.8的结构是一样的,数树。Hashtable和jdk1.8之前的HashMap的底层数据结构类似,都是采用数组+链表的形式, 数组是hashMap的主体,链表则是为了解决哈希冲突。
  • 实现线程安全的方法:①在1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),到1.8直接使用数组+链表+红黑树的实现。
9.ConcurrentHasMap底层如何实现分布式锁

将同一个容器的划分为好多Segmet(段),段数就是并发度,某个线程执行某些操作只会获取一个段的锁,

而不会影响其他线程获取其他锁的段。

扩展: ConcurrentHashMap把整个容器分为许多个Segment(段),段数就是并发度,每个Segment类似于一个Hashtable,管理一个HashEntry数组,每个HashEntry是一个链表头,或者红黑树的根,其实Segment的数据结构类似于HashMap。

10.map常用方法
.put(Object key,Object value):将指定key-value添加或修改当前map对象中
.putAll(Map m):将集合m中所有的key-value添加到当前map中
.remove(Object key):删除指定key的key-value对,并返回当前key对应的value
.clear():清空当前map中的所有数据(clear方法只是把元素清除掉,但在堆上开辟的map还在)
.isEmpty():判断集合是否为空(底部是用size==0判断的)
.get(Object key):通过key获取value
.containsKey(Object key)/.containsValue(Object value)是否包含key/value
.size():得到集合键值对个数
.keySet():获取Map中所有的key,返回一个Set集合
.values():获取map中所有的value,返回一个Conllection

HashMap

1.put数据时key与已有数据的HashCode相等时会怎么样?

会产生哈希碰撞。

如果Hash码相同,则会通过equals方法进行比较key是否相同:

如果key值相同,则使用新的value代替旧的value;

如果key值不相同,则会在该节点的链表结构上新增一个节点(如果链表长度>=8并且数组节点数>=64 ,链表结构就会转化成红黑树)。

2.什么时候会产生hash碰撞?如何解决哈希碰撞?

只要通过hash函数计算所得到的两个元素的hash值相同就会产生hash碰撞。

HashMap在jdk8之前采用链表解决哈希碰撞,jdk8之后采用链表+红黑树解决哈希碰撞。

3.如果hashCode相同,那么如何存储key-value对?

当hashCode值相同时,就会进一步使用equals方法进行比较key是否相同。

如果key相同,那么新值覆盖旧值;

如果key不相同,则将新的key-value添加到HashMap中。

4.HashMap如何进行扩容?

当通过put方法不断进行数据添加时,如果元素个数超过了当前阈值就会进行扩容,默认扩容大小是原来的2倍,扩容之后会将原来的数据复制到新的数组中。

5.如果确定了要存储的元素个数n,设置多少的初始容量可以减少扩容导致的性能损失?

应该设置初始容量为 n/0.75 + 1 取整即可减少resize导致的性能损失。

Collections工具类

常用方法:

  • 排序
  • 查找,替换
  • 同步控制(不推荐)
排序操作
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
123456
查找,替换
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target)
boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素
1234567

集合注意事项

集合判空

集合判断是否为空,使用isEmpty()的方法,而不是size==0的方式

因为isEmpty()方法的可读性好,而且时间复杂度为O(1)

集合转化Map

使用Java.util.stream.Collectors类的toMap()方法转化为map集合时,要注释当value为null时会抛出NPE异常。

集合遍历

不要在foreach里面进行元素的remove/add操作,remove请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。

fail-fast 机制 :多个线程对 fail-fast 集合进行修改的时候,可能会抛出ConcurrentModificationException

集合去重

可以利用Set元素唯一的特性,进行集合去重,避免使用List的contains进行遍历去重或者判断包含操作

集合转数组

使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的类型完全一致,长度为0的空数组。

toArray(T[] array) 方法的参数是一个泛型数组,如果 toArray 方法中没有传递任何参数的话返回的是 Object类 型数组。

数组转集合

使用工具类Arrays.asList()把数组转化成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常。


集合知识补充

一, 先来看几个变量、常量、静态变量、静态常量:
1)链表与红黑树互相转化的阀值:
 //将链表转化为红黑树的阀值
 static final int TREEIFY_THRESHOLD = 8;

 //将红黑树转回为链表的阀值
 static final int UNTREEIFY_THRESHOLD = 6;

 //将链表转为红黑树的最小entry数组的长度为 64
 //当链表的长度 >= 8,且entry数组的长度>= 64时,才会将链表转为红黑树。
 static final int MIN_TREEIFY_CAPACITY = 64;
2) Map的默认初始化容量以及容量极限:
//HashMap默认的初始容量大小--16,容量必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//HashMap的容量极限,为2的30次幂;
static final int MAXIMUM_CAPACITY = 1 << 30;
3) 默认负载因子、实际元素数量:
//负载因子的默认大小,
//元素的数量/容量得到的实际负载数值与负载因子进行对比,来决定容量的大小以及是否扩容;
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//HashMap的实际元素数量
transient int size;
4)table——Entry数组
//Node是Map.Entry接口的实现类,可以将这个table理解为是一个entry数组;
//每一个Node即entry,本质都是一个单向链表
transient Node<K,V>[] table;
5)Map已经修改的次数、下一次扩容的大小、存储负载因子的常量:
//HashMap已在结构上修改的次数 结构修改是指更改 HashMap 中的映射数量或以其他方式修改其内部结构(例如,重新散列)的那些
transient int modCount;

//下一次HashMap扩容的阀值大小,如果尚未扩容,则该字段保存初始entry数组的容量,或用零表示
int threshold;

//存储负载因子的常量,初始化的时候将默认的负载因子赋值给它;
final float loadFactor;

5.5)什么是Map已经修改的次数?

​ modCount用于记录HashMap的修改次数,在HashMap的put(),get(),remove(),Interator()等方法中,都使用了该属性。

​ 由于HashMap不是线程安全的,所以在迭代的时候,会将modCount赋值到迭代器的expectedModCount属性中,然后进行迭代;

​ 如果在迭代的过程中HashMap被其他线程修改了,modCount的数值就会发生变化,这个时候expectedModCount和ModCount不相等,迭代器就会抛出ConcurrentModificationException()并发修改异常

二,链表转红黑树

为什么要将链表转为红黑树:

当这个链表过长了,查找这个链表上的元素的时候自然会变慢,所以链表如果过长了会影响到HashMap的性能,

所以后来在Java8中,当链表过长时,会将该链表自动转为红黑树,红黑树是一个自平衡二叉树,能够优化查找的性能。

什么时候将链表转换为红黑树

①,当链表的长度 >= 8,并且entry数组的长度>= 64时,才会将链表转为红黑树。

②,当链表的长度 >= 8,但是entry数组的长度< 64时,不转为红黑树,而是调用resize方法进行扩容;

什么时候将红黑树退化回为链表:

在resize()方法扩容的时候,在将原Node数组迁移到扩容后的新Node数组的时候,如果该Node元素是一个红黑树,则对其进行拆分、然后才迁移到新的Node数组中,如果拆分之后的子树的数量小于等于6了,则将该子树转回为链表结构;


如何通过不改变引用的指向来修改内容

String str = new String("abc")在不改变引用指向的前提下输出“abcd”

public class No1 {
    public static void main(String[] args) throws Exception{
    String str = new String("abc");
    .....
    System.out.println(str); //abcd
}

只要通过String源码可以看到,它是通过参数赋值给value属性,value为一个char[]数组,

即可通过反射来获取value字段,将字段值直接修改为abcd即可:

public class No1 {
    public static void main(String[] args) throws Exception{

        String str = new String("abc");
       
        //通过反射拿到str里面的value字段
        Field value = str.getClass().getDeclaredField("value");
        //修改权限为true
        value.setAccessible(true);
        //将abc直接修改为abcd并转换为字符数组
        value.set(str,"abcd".toCharArray());

        System.out.println(str);
    }

代码解析:

setAccessible()方法不属于Field,它属于AccessibleObject,
Field通过extends AccessibleObject来获得setAccessible()方法;

getDeclaredField("value"):
java.lang.Class类的getDeclaredField()方法用于获取此类的指定字段。该方法以Field对象的形式返回此类的指定字段。
    
setAccessible(true):
该方式是用来设置获取权限的。
如果 accessible 标志被设置为true,那么反射对象在使用的时候,不会去检查Java语言权限控制(private之类的);
如果设置为false,反射对象在使用的时候,会检查Java语言权限控制。
需要注意的是,设置为true会引起安全隐患。

String底层源码解析:

public String(String original) {
     this.value = original.value;
     this.hash = original.hash;
 }
// 调用String类的构造方法,通过源码可以看到,它是把传进去的值赋值给value属性
 public final class String
         implements java.io.Serializable, Comparable<java.lang.String>, CharSequenc
     /** The value is used for character storage. */
     private final char value[];
     ......
// value属性为一个char数组

冒泡排序、选择排序、插入排序

冒泡排序演示:

public class No6 {
    public static void main(String[] args) {
        int[] ary = new int[100000];
        for (int i = 0; i < ary.length; i++) {
            int ran = (int) (Math.random()*200000);
            ary[i] = ran;
        }
9
        long t1 = System.currentTimeMillis();
        /**
         * 冒泡外层:每一轮都是从第一个元素开始比较,所以起始下标永远都是0
         * 外层:比较轮数
         * 内层:元素下标
         */
        //冒泡排序 耗时:21327ms
        for (int i = 0; i < ary.length; i++) {
            for (int j = 0; j < ary.length-1-i; j++) {
                if (ary[j]<ary[j+1]){
                    int tmp = ary[j];
                    ary[j]=ary[j+1];
                    ary[j+1]=tmp;
                }
            }
        }
        long t2 = System.currentTimeMillis();
        System.out.println("耗时:"+(t2-t1)+"ms");
    }
}

选择排序演示:

public class No6 {
    public static void main(String[] args) {
        int[] ary = new int[100000];
        for (int i = 0; i < ary.length; i++) {
            int ran = (int) (Math.random()*200000);
            ary[i] = ran;
        }

        long t1 = System.currentTimeMillis();
        /**
         * 选择排序:
         * 选择排序(Selection sort)是一种简单直观的排序算法。
         * 它的工作原理是:
         * 第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,
         * 然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。
         * 以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
         *
         */
        //选择排序 耗时:6139ms
        //外层:i表示对该位置求最小值
        for (int i = 0; i < ary.length; i++) {
            int min = 1;
            //内层:j表示对i后的每个元素进行遍历,判断其与min指向元素的大小
            for (int j = i+1; j < ary.length; j++) {
                if (ary[j]<ary[min]){
                    min = j;
                }
            }
            //将min指向的元素和i位置的元素互换
            if (min!=i){
                int tmp = ary[i];
                ary[i] = ary[min];
                ary[min] = tmp;
            }
        }
        long t2 = System.currentTimeMillis();
        System.out.println("耗时:"+(t2-t1)+"ms");
    }
}

插入排序演示

public class No6 {
    public static void main(String[] args) {
        int[] ary = new int[100000];
        for (int i = 0; i < ary.length; i++) {
            int ran = (int) (Math.random()*200000);
            ary[i] = ran;
        }

        long t1 = System.currentTimeMillis();
        /**
         * 插入排序:
         * 将序列分为排好序和未排序部分,对未排序部分元素进行遍历,
         * 将遍历到的元素插入到排好序元素的适当位置,通过比较和交换来实现
         */
        //插入排序 耗时:2149ms
        for (int i = 1; i < ary.length; i++) {
            for (int j = i; j>0 && ary[j]<ary[j-1]; j--) {
                int tmp = ary[j];
                ary[j] = ary[j-1];
                ary[j-1] = tmp;
            }
        }

        long t2 = System.currentTimeMillis();
        System.out.println("耗时:"+(t2-t1)+"ms");
    }
}

Java中八种数据结构

  • 哈希表(Hash)
  • 队列(Queue)
  • 树(Tree)
  • 堆(Heap)
  • 数组(Array)
  • 栈(Stock)
  • 链表(Linked List)
  • 图(Graph)

img

哈希表(Hash)

哈希表也叫散列表,是一种可以通过关键码值(Key-Value)直接访问的数据结构,可以实现快速查询、插入、删除。

数组类型的数据结构在插入和删除时时间复杂度高;链表类型的数据结构在查询时时间复杂度高;而哈希表结合了数组与链表的优势。

在jdk8中,Java中经典的HashMap,以数组+链表+红黑树构成。

哈希函数在哈希表中起着关键作用,能够将任意长度的输入转为定长的输出(哈希值)。通过哈希函数,能够快速地对数据元素进行定位。

哈希值并不是具有唯一性,在某些情况下Hash值会冲突,HashMap在Hash冲突时,会将元素在数组的位置上添加为链表元素结点,当链表长度大于8时,链表会转换为红黑树。

队列(Queue)

类比水管,两端放开,一端入水,一端出水。

队列是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cPzOtJJo-1667037486895)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220802085955709.png)]

树(Tree)

树是一种非线性结构,由n(n>0)个有限结点组成有层次关系的集合。

术语:

二叉树:每个结点最多含有2个子树。

完全二叉树:除了最外层的结点,其他各层结点都达到最大数。

满二叉树

国内定义:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。
国外定义:如果一棵二叉树的结点要么是叶子结点,要么它有两个子结点,这样的树就是满二叉树。
二叉查找树

任意结点的左子树不为空,左子树所有结点的值均小于根结点的值。
任意结点的右子树不为空,右子树所有结点的值均大于根节点的值。
任意结点的左右子树也是一颗二叉查找树。
平衡二叉树:也称AVL树,当且仅当任何结点的两棵子树的高度差不大于1的二叉树。Java中HashMap的红黑树就是平衡二叉树!!!

B树:一种对读写优化的自平衡二叉树,在数据库的索引中常见的BTREE就是自平衡二叉树。

B+树:B+树是应文件系统所需而产生的B树的变形树。

所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字
所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。
有m个子树的中间节点包含有m个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引。
Java8中HashMap的红黑树
实质上就是平衡二叉树,通过颜色约束二叉树的平衡:
1)每个节点都只能是红色或者黑色

2)根节点是黑色

3)每个叶节点(NIL 节点,空节点)是黑色的。

4)如果一个节点是红色的,则它两个子节点都是黑色的。也就是说在一条路径上不能出现相邻的两个红色节点。

5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

堆(Heap)

堆可以被看成一个树的数组对象,具有如下特点:

堆是一颗完全二叉树。
最大堆/大根堆:某个结点的值不大于父结点的值。
最小堆/小根堆:某个结点的值不小于父结点的值。

数组(Array)

数组是一种线性表的数据结构,连续的空间存储相同类型的数据。

  • 优点:

按照索引查询元素的速度很快;

按照索引遍历数组也很方便。

  • 缺点:

数组的大小在创建后就确定了,无法扩容;

数组只能存储一种类型的数据;

添加、删除元素的操作很耗时间,因为要移动其他元素。

栈(Stock)

栈可以类比为水桶,只有一端能够进出,遵循的先进后出的规则。

栈先进的元素进入栈底,读元素的时候从栈顶取元素。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-erQRTUNV-1667037486897)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220802085901651.png)]

链表(Linked List)

链表是一种线性表的链式存储方式,链表的内存是不连续的,前一个元素存储地址的下一个地址中存储的不一定是下一个元素。链表通过一个指向下一个元素地址的引用将链表中的元素串起来。

单向链表:单向链表是最简单的链表形式。我们将链表中最基本的数据称为节点(node),每一个节点包含了数据块和指向下一个节点的指针。
双向链表:顾名思义,双向链表就是有两个方向的链表。同单向链表不同,在双向链表中每一个节点不仅存储指向下一个节点的指针,而且存储指向前一个节点的指针。通过这种方式,能够通过在O(1)时间内通过目的节点直接找到前驱节点,但是同时会增加大量的指针存储空间。
循环链表:循环链表与双向链表相似,不同的地方在于:在链表的尾部增加一个指向头结点的指针,头结点也增加一个指向尾节点的指针,以及第一个节点指向头节点的指针,从而更方便索引链表元素。

图(Graph)

一个图就是一些顶点的集合,这些顶点通过一系列边结对(连接)。顶点用圆圈表示,边就是这些圆圈之间的连线。顶点之间通过边连接。

节点之间的关系是任意的,图中任意两个数据元素之间都有可能相关。


设计模式

设计模式有哪六大原则?

  • 单一职责原则
  • 开放封闭原则
  • 里氏替换原则
  • 依赖倒置原则
  • 迪米特原则
  • 接口隔离原则

设计模式的三大类

总体来说设计模式分为三大类:

创建型模式(5种):工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式(7种):适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式(11种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

img

其实还有两类:并发型模式和线程池模式。用一个图片来整体描述一下:

根据作用范围来分

根据模式是主要用于类上还是主要用于对象上来分,这种方式可分为类模式和对象模式两种。

类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。工厂方法、(类)适配器、模板方法、解释器属于该模式。
对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。

范围\目的创建型模式结构型模式行为型模式
类模式工厂方法(类)适配器模板方法、解释器
对象模式单例 原型 抽象工厂 建造者代理 (对象)适配器 桥接 装饰 外观 享元 组合策略 命令 职责链 状态 观察者 中介者 迭代器 访问者 备忘录

单例模式

单例模式? 懒汉模式? 饿汉模式? 它们之间的安全问题

单例模式:1.单例类只有一个实例;

​ 2.单例类必须自己创建自己的实例;

​ 3.单例类必须给所有其它对象提供这一实例。

单例模式的实现:1.创建一个私有的构造函数(防止其他类直接new此对象)

​ 2.创建一个私有的属性对象

​ 3.创建一个公共的对外暴露得单例对象

懒汉式 :非线程安全,懒汉比较懒只有当调用getInstance的时候,才会去初始化这个单例。

if判断以及其内存执行代码是非原子性的。其次,new Singleton()无法保证执行的顺序性。

public class SingletonLH {
    /**
     *是否 Lazy 初始化:是
     *是否多线程安全:否
     *实现难度:易
     *描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。
*因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
     *这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
     */
    private static SingletonLH instance;
    private SingletonLH (){}
	public static SingletonLH getInstance() {
    if (instance == null) {
        instance = new SingletonLH();
    }
    return instance;
}
}

饿汉式:线程安全,饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了。

public class SingletonEH {
    /**
     *是否 Lazy 初始化:否
     *是否多线程安全:是
     *实现难度:易
     *描述:这种方式比较常用,但容易产生垃圾对象。
     *优点:没有加锁,执行效率会提高。
     *缺点:类加载时就初始化,浪费内存。
     *它基于 classloder 机制避免了多线程的同步问题,
     * 不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,
    * 在单例模式中大多数都是调用 getInstance 方法,
     * 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,
     * 这时候初始化 instance 显然没有达到 lazy loading 的效果。
     */
    private static SingletonEH instance = new SingletonEH();
    private SingletonEH (){}
    public static SingletonEH getInstance() {
        System.out.println("instance:"+instance);
        System.out.println("加载饿汉式....");
        return instance;
    }
}

保证懒加载的线程安全

我们首先想到的就是使用synchronized关键字。synchronized加载getInstace()函数上确实保证了线程的安全。但是,如果要经常的调用getInstance()方法,不管有没有初始化实例,都会唤醒和阻塞线程。为了避免线程的上下文切换消耗大量时间,如果对象已经实例化了,我们没有必要再使用synchronized加锁,直接返回对象。

public class Singleton {
       private static Singleton instance = null;
       private Singleton() {
       };
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     instance = new Singleton();
              }
              return instance;
       }
}

我们把sychronized加在if(instance==null)判断语句里面,保证instance未实例化的时候才加锁

public class Singleton {
       private static Singleton instance = null;
       private Singleton() {
       };
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     synchronized (Singleton.class) {
                           if (instance == null) {
                                  instance = new Singleton();
                           }
                     }
              }
              return instance;
       }
}

new一个对象的代码是无法保证顺序性的,因此,我们需要使用另一个关键字volatile保证对象实例化过程的顺序性。

public class Singleton {
       private static volatile Singleton instance = null;
       private Singleton() {};
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     synchronized (instance) {
                           if (instance == null) {
                                  instance = new Singleton();
                           }
                     }
              }
              return instance;
       }
}

到此,我们就保证了懒加载的线程安全。

工厂方法模式

1、工厂方法模式

创建对象的过程不再由当前类实例化,而是由工厂类完成,在工厂类中只需要告知对象类型即可。工厂模式中必须依赖接口

简单工厂模式

简单工厂模式的工厂类一般是使用静态方法,通过接收的参数的不同来返回不同的对象实例。不修改代码的话,是无法扩展的。

img

以生产“电脑”为例,电脑有办公的功能,可以生产一体机或笔记本

代码与静态工厂一样

2.静态工厂模式

img

//电脑接口
public interface Computer {
    //电脑办公
    public void work();
}
//笔记本
public class PersonComputer implements  Computer{
 
    @Override
    public void work() {
        System.out.println("这是笔记本电脑,正在办公");
    }
}
//一体机
public class WorkComputer implements  Computer{
 
    @Override
    public void work() {
        System.out.println("这是一体机正在办公");
    }
}
//用于生产电脑的工厂 (这个工厂既可以生产台式机也可以生产笔记本)
public class ComputerFactory {
 
    /**
     * 根据不同的类型 生产不同的产品
     * @param type
     * @return
     */
    public Computer produce(String type){
        Computer computer =null;
        if(type.equals("personComputer")){
            computer = new PersonComputer();
        }else if(type.equals("workComputer")){
            computer = new WorkComputer();
        }else{
            System.out.println("不能生产");
        }
        return computer;
    }
//静态工厂方法模式
public class ComputerFactory2 {
    /**
     *  静态工厂方法
     * @param type
     * @return
     */
    public static Computer produce(String type){
        // 定义一个接口的引用    通过接口new 一个实现类的对象
        // 提高扩展性
        Computer computer=null;
         if(type.equals("workComputer")){
             computer = new WorkComputer();
         }else if(type.equals("personComputer")){
             computer = new PersonComputer();
         }else{
             System.out.println("不能创建对象");
         }
         return computer;
    }
}
//测试类
public class Test1 {
 public static void main(String[] args) {
         // 通过工厂类创建对象
        ComputerFactory factory = new ComputerFactory();
        // 要对象 找工厂
        Computer computer1 = factory.produce("workComputer");
        computer1.work();
        // 创建笔记本
        Computer computer2 = factory.produce("personComputer");
        computer2.work();
 
        Computer computer3 = ComputerFactory2.produce("workComputer");
        computer3.work();
 }
}

3.3工厂方法模式

工厂方法是针对每一种产品提供一个工厂类。通过不同的工厂实例来创建不同的产品实例。在同一等级结构中,支持增加任意产品。

img

例如:

img

//汽车接口
public interface Car {
    public void  showInfo();
}
public class AudiCar implements Car {
    @Override
    public void showInfo() {
        System.out.println("这是一台奥迪汽车。。");
    }
}
public class BMWCar implements Car {
    @Override
    public void showInfo() {
        System.out.println("这是一台宝马汽车。");
    }
}
/**
生产汽车的工厂接口
**/
public interface CarFactory {
    public Car produce();
}
public class AudiCarFactory implements  CarFactory {
    @Override
    public Car produce() {
 
        return  new AudiCar();// 这里AudiCar是Car的实现类
    }
}
public class BMWCarFactory implements CarFactory {
    @Override
    public Car produce() {
        return new BMWCar();// 因为BWMCar是Car的实现类
    }
}
public class Test1 {
        public static void main(String[] args) {
            //先创建 汽车工厂
            CarFactory bmwFactory = new BMWCarFactory();
            // 这个工厂生产的汽车就是 宝马
            Car bmw = bmwFactory.produce();
            bmw.showInfo();
    
            //这个模式对于同一级别的产品,可扩展性高
            //可以扩展不同品牌的汽车,此时不需要修改代码,只需要增加代码即可
            // 创建一个新的品牌汽车  大众汽车
    
            CarFactory dazhongFactory = new DazhongCarFactory();
            Car car = dazhongFactory.produce();
            car.showInfo();
        }
    }
 

抽象工厂模式

对于在工厂方法的基础上,对同一个品牌的产品有不同的分类,并对分类产品创建的过程 ,一个汽车产品 会分为不同的种类(迷你汽车 ,SUV汽车 )

img

/**
 * 迷你汽车接口
 */
public interface MiniCar {
    public void showInfo();
}
/**
 * SUV汽车接口
 */
public interface SUVCar {
    public void showInfo();
 
}
public class AudiMiniCar implements  MiniCar {
    @Override
    public void showInfo() {
        System.out.println("这是奥迪迷你汽车 ");
    }
}
public class BMWMiniCar implements  MiniCar {
    @Override
    public void showInfo() {
        System.out.println("这是宝马Cooper迷你汽车");
    }
}
public class AudiSUVCar implements  SUVCar {
    @Override
    public void showInfo() {
        System.out.println("这是一辆 奥迪SUV汽车");
    }
}
public class BMWSUVCar implements  SUVCar {
    @Override
    public void showInfo() {
        System.out.println("这宝马的SUV系列");
    }
}
public interface CarFactory {
    //生成不同型号的汽车 ,两条产品线
    public MiniCar produceMiniCar();
 
    public SUVCar produceSUVCar();
}
public class AudiCarFactory implements  CarFactory {
    @Override
    public MiniCar produceMiniCar() {
        return new AudiMiniCar();
    }
 
    @Override
    public SUVCar produceSUVCar() {
        return new AudiSUVCar();
    }
}
public class BMWCarFactory implements  CarFactory {
    // 生成迷你汽车的方法,返回MiniCar
    @Override
    public MiniCar produceMiniCar() {
        return new BMWMiniCar();
    }
    //生成SUV汽车的方法, 返回SUVCar
    @Override
    public SUVCar produceSUVCar() {
        return new BMWSUVCar();
    }
}
/**
 * 测试类
 */
public class Test1 {
    public static void main(String[] args) {
        //创建宝马迷你汽车  找工厂
        CarFactory factory = new BMWCarFactory();
        MiniCar car = factory.produceMiniCar();
        car.showInfo();
    }
}

总结

对于简单工厂,工厂方法模式和抽象工厂的区别和用途

★工厂模式中,重要的是工厂类,而不是产品类。产品类可以是多种形式,多层继承或者是单个类都是可以的。但要明确的,工厂模式的接口只会返回一种类型的实例,这是在设计产品类的时候需要注意的,最好是有父类或者共同实现的接口。

★使用工厂模式,返回的实例一定是工厂创建的,而不是从其他对象中获取的。

★工厂模式返回的实例可以不是新创建的,返回由工厂创建好的实例也是可以的。

区别

1、对于简单工厂,用于生产同一结构中的任意产品,对于新增产品不适用。

2、对于工厂方法,在简单工厂的基础上,生产同一等级结构中笃定产品,可以支持新增产品。

3、抽象工厂,用于生产不同种类(品牌)的相同类型(迷你,SUV),对于新增品牌可以,不支持新增类型


JVM

JVM构成

  • 类加载器: 负责加载类到内存中
  • 运行时数据区: 储存数据 对象,方法等等

img

  • 执行引擎: 负责解释执行字节码,GC操作等
  • 本地库接口: 融合其他语言为Java所用
JVM运行内存如何划分?
  • 堆内存
  • 栈内存
  • 程序计数器
  • 方法区

什么是JVM调优

简单理解,JVM调优主要就是为了解决系统运行时慢、卡顿、OOM、死锁等问题。

其实上面所说的问题存在很多方面的原因,比如网络波动导致响应时间慢、数据库查询慢、死锁等,今天我们主要分析JVM层面的,而JVM调优,主要是为了减少Full GC问题,也就是针对堆内存进行优化。

jvm调优涉及到两个很重要的概念:吞吐量和响应时间。jvm调优主要是针对他们进行调整优化,达到一个理想的目标,根据业务确定目标是吞吐量优先还是响应时间优先。

吞吐量:用户代码执行时间/(用户代码执行时间+GC执行时间)。指系统在单位时间内处理请求的数量。对于并发系统,通常需要用吞吐 量作为性能指标。
响应时间:整个接口的响应时间(用户代码执行时间+GC执行时间),stw时间越短,响应时间越短。指系统对请求作出响应的时间。对 于单用户的系统,响应时间可以很好地度量系统的性能。

Stop-the-World,简称STW
指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应, 
有点像卡死的感觉,这个停顿称为STW。

一、调优步骤

​ 调优的前提是熟悉业务场景,先判断出当前业务场景是吞吐量优先还是响应时间优先。调优需要建立在监控之上,由压力测试来判断是否达到业务要求和性能要求。

简单描述就是牺牲响应时间来满足内存要求,或者是增加内存来满足响应时间

调优的步骤大致可以分为:

1.熟悉业务场景,了解当前业务系统的要求,是吞吐量优先还是响应时间优先;

2.选择合适的垃圾回收器组合,如果是吞吐量优先,则选择ps+po组合;如果是响应时间优先,在1.8以后选择G1,在1.8之前选择ParNew+CMS组合;

在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加ParallelOld收集器这个组合

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space`这个异常吧,没错,

它就是大名鼎鼎的OOM(堆内存溢出)

一、CMS定义和特点
CMS收集器是一种以获取最短停顿时间为目标的收集器。它的优点是并发收集和低停顿。
二、CMS 基于哪种GC算法实现的
CMS主要是基于标记清除算法实现的
三、标记清除算法详解
算法主要是分为 标记 和 清除 两个阶段实现,在标记阶段 从 GC root出发通过可达性分析将需要被清除的对象标记起来,在清除阶段全部进行清除,这种算法有两个比较显著的一个缺点,一个是效率问题,一个是空间问题,标记-清除算法效率其实并不理想,这与它需要事先将需要清除的对象标记起来有关系,再一个就是空间的问题,该算法在清理的过程中会产生许多的空间碎片,这对内存是一种不小的浪费。为了解决空间碎片的问题 jvm引入了标记-整理算法,这将在之后的文章中介绍。
四、CMS 回收的四个阶段
(1)、初始标记
根据可达性分析从 GC roots 出发标记待清理对象的过程
(2)、并发标记
并发 从 GC roots tracing 的过程
(3)、重新标记
并发表基过程中由于 程序运行而导致标记产生变动的那一部分对象的重新标记
(4)、并发清除
清除标记对象的过程

3.规划内存需求,只能进行大致的规划。

4.CPU选择,在预算之内性能越高越好;

5.根据实际情况设置升级年龄,最大年龄为15;

为了避免这种朝生夕死的对象进入老年代,我们可以加大一下年轻代的容量和减少对象进入老年代的年龄阈值。

6.设定日志参数:-Xloggc:/path/name-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogs=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCauses

-XX:+UseGCLogFileRotation:GC文件循环使用

-XX:NumberOfGCLogs=5:使用5个GC文件

-XX:GCLogFileSize=20M:每个GC文件的大小

上面这三个参数放在一起代表的含义是:5个GC文件循环使用,每个GC文件20M,总共使用100M存储日志文件,当5个GC文件都使用完毕以后,覆盖第一个GC日志文件,生成新的GC文件。

二、CPU使用率飙高问题

​ 当cpu经常飙升到100%的使用率,那么证明有线程长时间占用系统资源不进行释放,需要定位到具体是哪个线程在占用,定位问题的步骤如下(linux系统):

​ 1.使用top命令查看当前服务器中所有进程(jps命令可以查看当前服务器运行java进程),找到当前cpu使用率最高的进程,获取到对应的pid;

2.然后使用top -Hp pid,查看该进程中的各个线程信息的cpu使用,找到占用cpu高的线程pid

3.使用jstack pid打印它的线程信息,需要注意的是,通过jstack命令打印的线程号和通过top -Hp打印的线程号进制不一样,需要进行转换才能进行匹配,jstack中的线程号为16进制,而top -Hp打印的是10进制。

使用jastack命令分析线程信息的时候需要关注线程对应的运行状态:

runnable代表当前线程正在运行,waiting代表当前线程正在等待,该状态需要进行特殊关注wait fot 后面的线程号,因为如果当前处于

waiting状态的程序长时间处于等待状态,那么就需要知道它在等待哪个线程结束,也就是wait for后面的线程号(jdk版本不同,单词可能不一样,总之就是在日志中找到它等待的线程号)然后根据线程号找到对应的线程,去查看当前线程有什么问题。


三、内存标高问题

​ 内存飙高一般都是堆中对象无法回收造成,因为java中的对象大部分存储在堆内存中。

其实也就是常见的oom问题(Out Of Memory)。

指令
1.jinfo pid,可以查看当前进行虚拟机的相关信息列举出来,如下图
2.jstat -gc pid ms,多长毫秒打印一次gc信息,打印信息如下,里面包含gc测试,年轻代/老年带gc信息等:
3.jmap -histo pid  head -20,查找当前进程堆中的对象信息,加上管道符后面的信息以后,代表查询对象数量最多的20个:

jmap -dump:format=b,file=xxx pid,可以生成堆信息的文件,但是这个命令不建议在生产环境使用,因为当内存较大时,执行该命令会占用大量系统资源,甚至造成卡顿。建议在项目启动时添加下面的命令,在发生oom时自动生成堆信息文件:-XX:+HeapDumpOnOutOfMemory。如果需要在线上进行堆信息分析,如果当前服务存在多个节点,可以下线一个节点,生成堆信息,或者使用第三方工具,阿里的arthas。

四、jvm调优常用参数

​ 通用GC参数
​ -Xmn:年轻代大小 -Xms:堆初始大小 -Xmx:堆最大大小 -Xss:栈大小

    -XX:+UseTlab:使用tlab,默认打开,涉及到对象分配问题

    -XX:+PrintTlab:打印tlab使用情况

    -XX:+TlabSize:设置Tlab大小

    -XX:+DisabledExplictGC:java代码中的System.gc()不再生效,防止代码中误写,导致频繁触动GC,默认不起用。

    -XX:+PrintGC(+PrintGCDetails/+PrintGCTimeStamps)打印GC信息(打印GC详细信息/打印GC执行时间)

    -XX:+PrintHeapAtGC打印GC时的堆信息

    -XX:+PrintGCApplicationConcurrentTime 打印应用程序的时间

    -XX:+PrintGCApplicationStopedTime 打印应用程序暂停时间

    -XX:+PrintReferenceGC 打印回收多少种引用类型的引用

    -verboss:class 类加载详细过程

    -XX:+PrintVMOptions 打印JVM运行参数

    -XX:+PrintFlagsFinal(+PrintFlagsInitial)  -version  grep 查找想要了解的命令,很重要

    -X:loggc:/opt/gc/log/path  输出gc信息到文件

    -XX:MaxTenuringThreshold  设置gc升到年龄,最大值为15

    parallel常用参数
    -XX:PreTenureSizeThreshold 多大的对象判定为大对象,直接晋升老年代

    -XX:+ParallelGCThreads 用于并发垃圾回收的线程

    -XX:+UseAdaptiveSizePolicy 自动选择各区比例

    CMS常用参数
    -XX:+UseConcMarkSweepGC 使用CMS垃圾回收器

    -XX:parallelCMSThreads CMS线程数量

    -XX:CMSInitiatingOccupancyFraction 占用多少比例的老年代时开始CMS回收,默认值68%,如果频繁发生serial old,适当调小该比例,降低FGC频率

    -XX:+UseCMSCompactAtFullCollection 进行压缩整理

    -XX:CMSFullGCBeforeCompaction 多少次FGC以后进行压缩整理

    -XX:+CMSClassUnloadingEnabled 回收永久代

    -XX:+CMSInitiatingPermOccupancyFraction 达到什么比例时进行永久代回收

    GCTimeTatio 设置GC时间占用程序运行时间的百分比,该参数只能是尽量达到该百分比,不是肯定达到

    -XX:MaxGCPauseMills GCt停顿时间,该参数也是尽量达到,而不是肯定达到

    G1常用参数
    -XX:+UseG1 使用G1垃圾回收器

    -XX:MaxGCPauseMills GCt停顿时间,该参数也是尽量达到,G1会调整yong区的块数来达到这个值

    -XX:+G1HeapRegionSize 分区大小,范围为1M~32M,必须是2的n次幂,size越大,GC回收间隔越大,但是GC所用时间越长

    G1NewSizePercent 新生代所占最小比例,默认5%

    G1MaxNewSizePercent 新生代所占最大比例,默认60%

    GCTimeRatio GC时间比例,此值为建议值,G1会调整堆大小来尽量达到这个值

    ConcGCThreads GC线程数量

    InitiatingHeapOccupancyPercent 启动G1的堆空间占用比例

五、JVM的作用

首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在

运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

GC的垃圾回收算法?

垃圾回收只涉及到堆内存。

新生代使用复制算法:分配同等大小的内存空间,标记被GC Root引用的对象,将引用的对象连续的复制到新的内存空间,

​ 清除原来的内存空间。

老年代使用:
标记清除法:标记没有被GC Root引用的对象,清除被标记位置的内存。
标记整理法:标记没有被GC Root引用的对象。


双亲委派

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,
一层层的向上传递,如果上级的类加载器都没有加载,自己才会去加载这个类。

向上委派,向下查找


Spring

Spring、SpringBoot、SpringMVC的区别

spring是一个开源的轻量级的java开发框架,主要负责创建对象并管理对象,它的核心功能是IOC和AOP;

SpringBoot是基于Spring的一套快速开发整合包,类似于脚手架,可以快速搭建一个项目;

SpringMVC是基于Spring实现了servlet规范的MVC框架,用于Java Web开发;

Spring是什么,怎么理解它的aop,ioc

spring是一个开源的轻量级的java开发框架,主要负责创建对象并管理对象;
IOC:控制反转,Spring 的 IOC 的实现原理就是工厂模式加反射机制。把一个类放到spring容器去管理,对象的创建,初始化,销毁的工作都交给spring容器去做。由spring容器控制对象的生命周期。
AOP:面向切面编程,把系统分为核心关注点与横切关注点。核心主要是处理业务逻辑,横切主要是权限验证,日志,事务处理。

Spring AOP是基于代理实现的,默认标准的JDK 动态代理,这使得任何接口(或者接口的集合)可以被代理。
Spring AOP 也使用 CGLIB 代理。如果业务对象没有实现任何接口那么默认使用CGLIB

**JDK动态代理:**利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

**CGlib动态代理:**利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将处理对象类的class文件加载进来,通过修改其字节码生成子类来处理

SpringBean的作用域

  • singleton:默认作用域,单例bean,每个容器只有一个bean的实例

  • prototype:每一个bean请求创建一个实例

  • request:为每一个request请求创建一个实例,在请求完成以后,bean会消失并被垃圾回收器回收

  • session:与request范围类似,同一个session会话共享一个实例,不同会话使用不同实例

  • global-session:全局作用域,所有会话共享一个实例。如果想要声明让所有会话共享存储变量的话,

    那么这全局变量需要存储在global-session中


SpringBean的生命周期

1.Spring启动,查找并加载需要被Spring管理的Bean,进行Bean的实例化;

2.Bean实例化后对将Bean的应用和值注入到Bean的属性中;

3.如果Bean实现了BeanNameAware接口的话,Spring将Bean的ID传递给setBeaName()方法;

4.如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;

5.如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来;

6.如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforelization()方法;

7.如果Bean实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。如果Bean使用init-method声明了初始化方法,该方法也会被调用;

8.如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterlnitialization()方法;

9.此时,Bean已经准备就绪,可以被开发者使用了,它们会一直驻留在应用上下文中,知道应用上下文被销毁;

10.如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样 如果Bean使用destory-methos声明销毁方法,该方法也会被调用。


Spring三大核心组件

core:Core 组件作为 Spring 的核心组件,他其中包含了很多的关键类,其中一个重要组成部分就是定义了资源的访问方式

作用:访问资源,资源的加载

context:spring运行环境和IOC容器

作用:为Spring提供运行环境,Context是作为Spring的IOC容器。

bean:创建和定义bean

作用:1.Bean的创建 2.Bean的定义


Spring常用的注入方式

1.setter注入:创建applicationContext.xml文件,手动配置Bean对象,property为Bean对象赋值。
2.构造器注入:更改applicationContext.xml文件中的property为construct-arg
3.注解注入(属性注入):使用注解方式的属性注入Bean。加@Component注解,将其标记为组件,并使用@Value注解为各属性赋值;


@Resource和@Autowired

  • @Resource和@Autowired都可以用来装配bean,都可以用于字段或setter方法。
  • @Autowired默认按类型装配,默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false。
  • @Resource默认按名称装配,当找不到与名称匹配的bean时才按照类型进行装配。名称可以通过name属性指定,如果没有指定name属性,当注解写在字段上时,默认取字段名,当注解写在setter方法上时,默认取属性名进行装配。
    注意:如果name属性一旦指定,就只会按照名称进行装配。

@Autowire和@Qualifier配合使用效果和@Resource一样:

@Autowired(required = false) @Qualifier("example")
private Example example;

@Resource(name = "example")
private Example example;
  • @Resource装配顺序
  1. 如果同时指定name和type,则从容器中查找唯一匹配的bean装配,找不到则抛出异常
  2. 如果指定name属性,则从容器中查找名称匹配的bean装配,找不到则抛出异常
  3. 如果指定type属性,则从容器中查找类型唯一匹配的bean装配,找不到或者找到多个抛出异常
  4. 如果都不指定,则自动按照byName方式装配,如果没有匹配,则回退一个原始类型进行匹配,如果匹配则自动装配

简要对比表格

注解对比@Resource@Autowire
注解来源JDKSpring
装配方式优先按名称优先按类型
属性name、typerequired

Spring框架中使用了那些设计模式

(1)工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象

(2)单例模式:Bean默认为单例模式

(3)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术

(4)策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略

(5)模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。

​ 比如RestTemplate, JmsTemplate, JpaTemplate

(6)适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller

(7)观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。

(8)桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,

​ 客户在每次访问中根据需要会去访问不同的数据库


Spring事务

**声明式事务(常用):**建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

编程式事务:控制的细粒度更高,我们能够精确的控制事务的边界,事务的开始和结束完全取决于我们的需求,但这种方式存在一个致命的缺点,那就是事务规则与业务代码耦合度高,难以维护,因此我们很少使用这种方式对事务进行管理。


Spring事务4个特性(ACID):

**原子性(Atomicity):**一个事务是一个不可分割的工作单位,事务要么全部成功、要么全部失败;

**一致性(Consistency)😗*事务必须保证从一个一致性状态到另一个一致性状态,

一致性和原子性密切相关,其它三个特性是为了实现一致性;

**隔离性(Isolation)😗*一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相打扰。

**持久性(Durability)😗*持久性也称为永久性,指一个事务一旦提交,它对数据库中数据的改变就是永久性的,后面的其它操作和故障都不应该对其有任何影响。

事务允许我们将几个或一组操作组合成一个要么全部成功、要么全部失败的工作单元。如果事务中的所有的操作都执行成功,那自然万事大吉。但如果事务中的任何一个操作失败,那么事务中所有的操作都会被回滚,已经执行成功操作也会被完全清除干净,就好像什么事都没有发生一样。


Spring隔离级别

Spring 的事务隔离级别底层其实是基于数据库的,Spring 并没有自己的一套隔离级别。

(直接说数据库的隔离级别)

1.读未提交(Read uncommitted):特点:事务可以读取到其他事务未提交/未回滚前的数据,会产生脏读
什么是脏读:由于事务读取到了其他事务未提交/未回滚前的数据,导致读取的数据最终是不存在的,这个现象就叫做脏读.

2.读已提交(Read committed):特点:事务只能读取到其他事务提交/回滚后的数据,解决了脏读问题,但是会产生不可重复读问题.

什么是不可重复读:在事务A执行期间,其他事务对事务A访问的数据进行修改操作,导致事务A中前后两次读取相同的数据的结果是不一致的.这个现象就叫做不可重复读

3.可重复读(Repeatable read):解决了不可重复读问题,产生了新的问题 – 幻读
什么是幻读: 在事务A访问数据期间,其他事务执行了插入操作,导致事务A前后两次读取到的数据总量不一致,这个现象就叫做幻读.

4.串行化(Serializable ):解决了幻读问题,实现了多事务并发执行同步效果,所以这个隔离级别的并发执行效率是最低下的。


Spring事务传播机制

propagetion(/ ˌprɒpəˈɡeɪʃn /)

① PROPAGATION_REQUIRED**(required)**:(默认传播行为)如果当前没有事务,就创建一个新事务;

如果当前存在事务,就加入该事务。

② PROPAGATION_REQUIRES_NEW**(requires_new)**:无论当前存不存在事务,都创建新事务进行执行。

③ PROPAGATION_SUPPORTS**(supports)**:如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。

④ PROPAGATION_NOT_SUPPORTED**(not_supported)**:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

⑤ PROPAGATION_NESTED**(nested)**:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。

⑥ PROPAGATION_MANDATORY**(mandatory)**:如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。

⑦ PROPAGATION_NEVER**(never)**:以非事务方式执行,如果当前存在事务,则抛出异常。


Spring 怎么解决循环依赖的问题?

Spring 是通过提前暴露 bean 的引用来解决的;
Spring 首先使用构造器创建一个 “不完整” 的 bean 实例,
并且提前曝光该 bean 实例的 ObjectFactory.通过 ObjectFactory 我们可以拿到该 bean 实例的引用,
如果出现循环引用,我们可以通过缓存中的 ObjectFactory 来拿到 bean 实例,从而避免出现循环引用导致的死循环。


Spring三级缓存

singletonObjects <ConCurrentHashMap<>> 一级缓存

earlySingletonObjects <HashMap<>> 二级缓存

singletonFactories <HashMap<>> 三级缓存


Spring 事务的实现原理

Spring 事务的底层实现主要使用的技术:AOP(动态代理) + ThreadLocal + try/catch;
Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring 是无法提供事务功能的。
真正的数据库层的事务提交和回滚是通过binlog 或者 redo log 实现的。


JSON和XML之间的区别:

1、JSON是JavaScript Object Notation;XML是可扩展标记语言。
2、JSON是基于JavaScript语言;XML源自SGML。
3、JSON是一种表示对象的方式;XML是一种标记语言,使用标记结构来表示数据项。
4、JSON不提供对命名空间的任何支持;XML支持名称空间。
5、JSON支持数组;XML不支持数组。
6、XML的文件相对难以阅读和解释;与XML相比,JSON的文件非常易于阅读。
7、JSON不使用结束标记;XML有开始和结束标签。
8、JSON的安全性较低;XML比JSON更安全。
9、JSON不支持注释;XML支持注释。
10、JSON仅支持UTF-8编码;XML支持各种编码。


SpringMVC

简述SpringMVC运行流程

  • SpringMVC先将请求发送给DispatchServlet;
  • DispatchServlet查询一个或多个HandlerMapping,找到处理请求的Controllerl并把请求提交到对应的controller;
  • Controller进行业务逻辑处理后,会返回一个ModelAndView;
  • DispatchServlet查询一个或多个ViewResolver视图解析器,找到ModelAndView对象指定的视图对象;
  • 视图对象负责渲染返回给客户端。

SpringMVC和Struts2的区别

1.SpringMVC基于方法开发,Struts2基于类开发。

2.SpringMVC可以进行单例开发,Struts2只能多例开发。

(SpringMVC默认controller是单实例,通过注解@Scope(“propotype”)变成了多实例);

3.Struts2的处理速度比SpringMVC慢,原因是Struts2使用了Struts标签,Struts标签由于设计原因,会出现加载数据慢的情况。


SpringBoot

SpringBoot自动加载

Spring核心组件

我们在SpringBoot的启动类上面可以看到注解 @SpringBootApplication ;

该注解表示这是一个SpringBootApplication的应用启动类的入口.类似于Java的main()方法。

进到 SpringBootApplication 注解内部。

在这个类上我们可见 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan 这三个比较不一样的注解。

@SpringBootConfiguration

在这里我们可以看到 @Configuration 这个注解。

其作用时告诉SpringBoot:SpringBootConfiguration 是一个配置类文件。

所以 @SpringBootConfiguration 作用同样时告诉SpringBoot:SpringBootApplication 是一个配置类文件。

@EnableAutoConfiguration

在这里我们可以看到 **@AutoConfigurationPackage **以及 **@Import(AutoConfigurationImportSelector.class) **这个注解。

@AutoConfigurationPackage:自动加载启动类下面的子包、配置组件类(repository、service、controller、component)和启动类

@Import(AutoConfigurationImportSelector.class) :(读取配置文件:spring.factories(工厂)),根据配置创建需要用到的组件(这里面共有127个组件),放到spring容器中,通过pom配置文件按需分配。如果这个组件不在这127个组件中,需要添加依赖,下载新的组件。

@ComponentScan

这个注解在Spring中很重要 ,它对应XML配置中的元素。

作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中


SpringCloud

Nacos注册中心

1.pom文件中添加依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

2.application-dev.yml文件中添加当前项目对nacos注册的配置

spring:
  application:
    # 当前Springboot项目的名称,用作注册中心服务的名称
    name: nacos-business
  cloud:
    nacos:
      discovery:
        # 定义nacos运行的路径
        server-addr: localhost:8848
        # ephemeral设置当前项目启动时注册到nacos的类型 true(默认):临时实例 false:永久实例
        ephemeral: true 

Nacos心跳机制

**临时实例:**默认情况下,启动服务后,每隔5秒会向nacos发送一个"心跳包",这个心跳包中包含了当前服务的基本信息,

Nacos收到这个"心跳包"如果发现这个服务的信息不在注册列表中,就进行注册,如果这个服务的信息在注册列表中

就表明这个服务还是健康的。

如果Nacos15秒内没接收到某个服务的心跳包,Nacos会将这个服务标记为不健康的状态;

如果30秒内没有接收到这个服务的心跳包,Nacos会将这个服务从注册列表中剔除。

**永久实例:**持久化实例启动时向nacos注册,nacos会对这个实例进行持久化处理,

心跳包的规则和临时实例一致,只是不会将该服务从列表中剔除。

Dubbo(RPC)

rpc:远程过程调用

通信协议

通信协议指的就是远程调用的通信方式

序列化协议

序列化协议指通信内容的格式,双方都要理解这个格式

发送信息是序列化过程,接收信息需要反序列化

Dubbo是Spring Cloud Alibaba提供的一个框架,能够实现微服务项目的互相调用

Dubbo的注册发现流程

1.首先服务的提供者启动服务到注册中心注册,包括各种ip端口信息,Dubbo会同时注册该项目提供的远程调用的方法;

2.服务的消费者注册到注册中心,订阅发现;

3.当有新的远程调用的方法注册到注册中心时,注册中心会通知服务的消费者有哪些新的方法;

4.PRC调用,在上面条件满足的情况下,服务的调用者无需知道ip和端口号,只需要服务名称就可以调用到服务提供者的方法。

Dubbo内置的负载均衡

Loadbalance:负载均衡

  • random loadbalance:随机分配策略(默认)

image-20220507144122475

随机生成随机数,在哪个范围内让哪个服务器运行

优点:算法简单,效率高,长时间运行下,任务分配比例准确

缺点:偶然性高,如果连续的几个随机请求发送到性能弱的服务器,会导致异常甚至宕机

  • round Robin Loadbalance:权重平均分配

如果几个服务器权重一致,那么就是依次运行

3个服务器 1>1 2>2 3>3 4>1

但是服务器的性能权重一致的可能性很小

所以我们需要权重评价分配

Dubbo2.6.4之前平均分配权重算法是有问题的

如果3个服务器的权重比5:3:1

1>1 2>1 3>1 4>1 5>1 6>2 7>2 8>2 9>3

10>1

Dubbo2.7之后更新了这个算法使用"平滑加权算法"优化权重平均分配策略

1655955337434

  • leastactive Loadbalance:活跃度自动感知分配

记录每个服务器处理一次请求的时间,按照时间比例来分配任务数,运行一次需要时间多的分配的请求数较少

  • consistanthash Loadbalance:一致性hash算法分配

根据请求的参数进行hash运算,以后每次相同参数的请求都会访问固定服务器。因为根据参数选择服务器,不能平均分配到每台服务器上

使用的也不多。

Seata

在业务中,必须保证数据库操作的原子性,也就是当前业务的所有数据库操作要么都成功,要么都失败

单体项目使用Spring声明式事务来解决本地的事务问题

但是现在是微服务环境,一个业务可能涉及多个模块的数据库操作

这种情况就需要专门的微服务状态下解决事务问题的"分布式事务"解决方案

Seata就是这样的产品

Seata事务的四种模式:AT、TCC、SAGA、XA

Seata构成部分包含

  • 事务协调器TC
  • 事务管理器TM
  • 资源管理器RM

Sentinel

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

添加pom依赖如下:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

application-dev.yml修改配置如下:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8080 # 配置Sentinel仪表台的位置
        port: 8721 # 真正执行限流的端口也要设置一下,注意这个端口其他微服务项目不能相同

限流:简单来说就是设置一秒时间内限制的请求数,当超过了这个值时,可以将请求放入消息队列,也可以设置降级。

熔断降级:自定义返回最低限度的异常(服务器忙,请稍后再试!)

StockController中编写代码如下:

@Autowired
private IStockService stockService;
@PostMapping("/reduce/count")
@ApiOperation("减少商品库存业务")
// @SentinelResource标记的方法会被Sentinel监控
// value的值是这个监控的名称,我们可以在"仪表台"中看到
// blockHandler的值指定了请求被限流时运行的方法名称
@SentinelResource(value = "减少库存方法(控制器)",blockHandler = "blockError")
public JsonResult reduceCommodityCount(StockReduceCountDTO stockReduceCountDTO){
    stockService.reduceCommodityCount(stockReduceCountDTO);
    return JsonResult.ok("商品库存减少完成!");
}
// Sentinel 限流方法应该满足如下要求
// 1.必须是public修改
// 2.返回值类型必须和控制方法一致(JsonResult)
// 3.方法名称要和控制器方法限流注解中规定的名称一致(blockError)
// 4.参数列表必须和控制器一致,可以在所以参数后声明BlockException来获得限流异常
public JsonResult blockError(StockReduceCountDTO stockReduceCountDTO,
                             BlockException e){
    return JsonResult.failed(ResponseCode.BAD_REQUEST,"服务器忙,请稍后再试");
}

上面方法定义了被Sentinel限流时运行的方法

降级功能和我们之前学习的统一异常处理类有相似的地方,但是降级是Sentinel的功能

@PostMapping("/reduce/count")
@ApiOperation("减少商品库存业务")
// @SentinelResource标记的方法会被Sentinel监控
// value的值是这个监控的名称,我们可以在"仪表台"中看到
// blockHandler的值指定了请求被限流时运行的方法名称
@SentinelResource(value = "减少库存方法(控制器)",blockHandler = "blockError",
                    fallback = "fallbackError")
public JsonResult reduceCommodityCount(StockReduceCountDTO stockReduceCountDTO){
    // 生成随机出触发降级流程
    if(Math.random()<0.5){
        throw new 
          CoolSharkServiceException(ResponseCode.INTERNAL_SERVER_ERROR,"异常");
    }
    stockService.reduceCommodityCount(stockReduceCountDTO);
    return JsonResult.ok("商品库存减少完成!");
}
// 这个方法是Sentinel注解中fallback属性指定的降级方法
// 当前控制器方法运行发生异常时,Sentinel会运行下面的降级方法
// 降级方法中,可以不直接结束请求,而去运行一些代替代码或者补救措施
// 让用户获得最低限度的响应
public JsonResult fallbackError(StockReduceCountDTO stockReduceCountDTO){
    return JsonResult.failed(ResponseCode.BAD_REQUEST,"因为运行异常,服务降级");
}

blockHandler和fallback的区别

两者都是不能正常调用资源返回值的顶替处理逻辑.

blockHander只能处理BlockException 流控限制之后的逻辑.

fallback处理的是资源调用异常的降级逻辑.


SpringGateway

Nacos对应Eureka 都是注册中心

Dubbo对应ribbon+feign都是实现微服务间调用

Sentinel对应Hystrix都是项目限流熔断降级组件

Gateway对应zuul都是项目的网关

Gateway不是阿里巴巴的而是Spring提供的

配置文件中配置开启动态路由功能即可

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 开启Spring Gateway的动态路由功能
          # 规则是根据注册到Nacos的项目名称作为路径的前缀,就可以访问到指定项目了
          enabled: true
  main:
    web-application-type: reactive
server:
  port: 10000

网关依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

SpringMvc依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

这两个依赖在同一个项目中时,默认情况下启动会报错

SpringMvc依赖中自带一个Tomcat服务器,而Gateway依赖中自带一个Netty服务器

因为在启动服务时这个两个服务器都想启动,会因为争夺7端口号和主动权而发生冲突

我们需要在yml文件中添加配置解决

spring:
  main:
    web-application-type: reactive

Elasticsearch

PageHelper的分页原理就是在程序运行时,在sql语句尾部添加limit关键字,并按照分页信息向limit后追加分页数据

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>

pageHelper底层分页原理

1、PageHelper首先将前端传递的参数保存到page这个对象中

2、接着将page的副本存放入ThreadLocal中,ThreadLocal很多地方叫做线程本地变量,为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。这样可以保证分页的时候,参数互不影响

3、PageHelper实现了mybatis提供的拦截器interceptor接口,调用其中的intercept方法,取得ThreadLocal的值

PageHelper在我们执行SQL语句之前动态的将SQL语句拼接了分页的语句,从而实现了从数据库中分页获取的过程。


JWT登录流程

SpringSecurity验证用户名和密码如果成功则生成一个JWT令牌,不保存在session里,而是直接响应到前端,前端接到登录成功的响应 将JWT保存在cookie或localStorage中。前端发送请求时将JWT保存在请求头中,请求会先到达Security框架提供的过滤链中进行过滤,一般情况下会在UsernamePasswordAuthenticationFilter过滤器前自定义过滤器,用来判断前端发送过来的请求是否携带jwt数据,如果没有携带jwt数据则直接响应到登录页面。如果携带 则直接放行到UsernamePasswordAuthenticationFilter过滤器验证用户名密码和权限,

通过验证则得到用户信息到控制层,控制器通过 @PreAuthorize(“hasRole(‘ROLE_user’)”)来判断用户是否具备相应的权限来访问具体的方法。


SpringSecurity认证流程:

在 UsernamePasswordAuthenticationFilter 过滤器认证成功之 后,会在认证成功的处理方法中将已认证的用户信息对象 Authentication 封装进 SecurityContext,并存入 SecurityContextHolder之后,响应会通过 SecurityContextPersistenceFilter 过滤器,该过滤器的位置在所有过滤器的最前面。

认证成功的响应通过 SecurityContextPersistenceFilter 过滤器时,会从 SecurityContextHolder 中取出封装了已认证用户信息对象 Authentication 的SecurityContext,放进 Session 中。当请求再次到来时,请求首先经过该过滤器,该过滤器会判断当前请求的 Session 是否存有 SecurityContext对象,如果有则将该对象取出再次放入 SecurityContextHolder 中,之后该请求所在的线程获得认证用户信息,后续的资源访问不需要进行身份认证;当响应再次返回时,该过滤器同样从 SecurityContextHolder 取出SecurityContext 对象,放入 Session 中。


Redis

缓存淘汰策略

Redis服务器繁忙时,有大量信息要保存

如果Redis服务器内存全满,再要往Redis中保存新的数据,就需要淘汰老数据,才能保存新数据

noeviction:返回错误**(默认)**

allkeys-random:所有数据中随机删除数据

volatile-random:有过期时间的数据库中随机删除数据

volatile-ttl:删除剩余有效时间最少的数据

allkeys-lru:所有数据中删除上次使用时间最久的数据

volatile-lru:有过期时间的数据中删除上次使用时间最久的数据

allkeys-lfu:所有数据中删除使用频率最少的

volatile-lfu:有过期时间的数据中删除使用频率最少的

缓存穿透

正常业务下,从数据库查询出的数据可以保存在Redis中

下次查询时直接从Redis中获得,大幅提高响应速度,提高系统性能

所谓缓存穿透,就是查询了一个数据库中都不存在的数据

我们Redis中没有这个数据,它到数据库查,也没有

如果这样的请求多了,那么数据库压力就会很大

前面阶段我们使用向Redis中保存null值,来防止一个查询反复穿透

但是这样的策略有问题

如果用户不断更换查询关键字,反复穿透,也是对数据库性能极大的威胁

使用布隆过滤器来解决这个问题

事先创建好布隆过滤器,它可以在进入业务逻辑层时判断用户查询的信息数据库中是否存在,如果不存在于数据库中,直接终止查询返回

缓存击穿

正常运行的情况,我们设计的应该在Redis中保存的数据,如果有请求访问到Redis而Redis没有这个数据

导致请求从数据库中查询这种现象就是缓存击穿

但是这个情况也不是异常情况,因为我们大多数数据都需要设置过期时间,而过期时间到时这些数据一定会从数据库中同步

击穿只是这个现象的名称,并不是不允许的

image-20220519104434652

缓存雪崩

上面讲到击穿现象

同一时间发生少量击穿是正常的

但是如果出现同一时间大量击穿现象就会如下图

image-20220519105212606

这种情况下,Mysql会短时间出现很多新的查询请求,这样就会发生性能问题

如何避免这样的问题?

因为出现这个问题的原因通常是同时加载的数据设置了相同的有效期

我们需要在设置有效期时添加一个随机数,大量数据就不会同时失效了,


Redis存储原理

Redis将内存划分为16384个槽(类似hash槽)

将数据(key)使用CRC16算法计算出一个在0~16383之间的值

将数据存到这个槽中

当再次使用这个key计算时,直接从这个槽获取,大幅提高查询效率

1657013889222

实际上这就是最基本"散列算法"

Redis集群

最小状态Redis是一台服务器

这台服务器的状态直接决定的Redis的运行状态

如果它宕机了,那么Redis服务就没了

系统面临崩溃风险

我们可以在主机运行时使用一台备机

主从复制

1657014182997

也就是主机(master)工作时,安排一台备用机(slave)实时同步数据,万一主机宕机,我们可以切换到备机运行

缺点,这样的方案,slave节点没有任何实质作用,只要master不宕机它就和没有一样,没有体现价值

读写分离

1657014449976

这样slave在master正常工作时也能分担Master的工作了

但是如果master宕机,实际上主备机的切换,实际上还是需要人工介入的,这还是需要时间的

那么如果想实现故障是自动切换,一定是有配置好的固定策略的

哨兵模式:故障自动切换

1657014722404

哨兵节点每隔固定时间向所有节点发送请求

如果正常响应认为该节点正常

如果没有响应,认为该节点出现问题,哨兵能自动切换主备机

如果主机master下线,自动切换到备机运行

但是这样的模式存在问题

1657014957753

但是如果哨兵判断节点状态时发生了误判,那么就会错误将master下线,降低整体运行性能

哨兵集群

上次课我们说了哨兵

如果哨兵服务器是一个节点,它误判master节点出现了故障,将master节点下线

但是master其实是正常工作的,整体系统效率就会大打折扣

我们可以将哨兵节点做成集群,由多个哨兵投票决定是否下线某一个节点

1657071387427

哨兵集群中,每个节点都会定时向master和slave发送ping请求

如果ping请求有2个(集群的半数节点)以上的哨兵节点没有收到正常响应,会认为该节点下线

分片集群

当业务不断扩展,并发不断增高时

有可能一个Master节点做写操作性能不足,称为了系统性能的瓶颈

这时,就可以部署多个Master节点,每个节点也支持主从复制

只是每个节点负责不同的分片

Redis0~16383号槽,

例如MasterA复制0~5000

MasterB复制5001~10000

MasterC复制10001~16383

一个key根据CRC16算法只能得到固定的结果,一定在指定的服务器上找到数据

1657072179480

有了这个集群结构,我们就能更加稳定和更加高效的处理业务请求了

为了节省哨兵服务器的成本,有些时候在Redis集群中直接添加哨兵功能,既master/slave节点完成数据读写任务的同时也都互相检测它们的健康状态

Redis数据一致性问题

①缓存数据插入时机:

对于服务器而言,查询数据步骤:

1、首先到缓存查询数据,如果数据存在,则直接获取数据返回。

2、如果缓存不存在,需要查询数据库,从数据库获取数据并插入缓存,将数据返回。

3、当第二次查询时,后续查询操作就可以查询缓存数据。

②更新数据时操作:

一:延时双删策略

1、先删除缓存再更新数据库

进行更新数据库数据时,先删除缓存,然后更新数据库,后续的请求再次读取数据时,会从数据库中读取数据更新到缓存。

存在问题:删除缓存之后,更新数据库之前,这个时间段内如果有新的请求过来,就会从数据库中读到旧的数据写入缓存,再次造成数据不一致,并且后续读操作都是旧数据。

2、先更新数据库再删除缓存

进行更新操作,先更新数据库,成功之后,再删除缓存,后续请求将新数据写回缓存

存在问题:更新MySQL和删除缓存这段时间内,请求读取的还是缓存内的旧数据,不过等数据库更新完成后,就会恢复一致。

二:异步更新缓存(基于Mysql binlog的同步机制)

1、异步更新缓存

数据库的更新操作完成后不直接操作缓存,将操作命令封装成消息放到消息队列里,然后由Redis自己去更新数据,消息队列保证数据操作数据的一致性,保证缓存数据的数据正常。


Redis支持的数据类型有哪些

Redis支持的数据类型主要有五种:

string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。

Redis如何实现数据持久化

内存的特征就是一旦断电,所有信息都丢失,Redis来讲,所有数据丢失,就需要从数据库重新查询所有数据,这个过程是慢的

更有可能,Redis本身是有新数据的,还没有和数据库同步就断电了

所以Redis支持了持久化方案,在当前服务器将Redis中的数据保存在当地硬盘上

Redis恢复策略有两种:

RDB 是 Redis 默认的持久化方案。当满足一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件 dump.rdb。

Redis 重启会通过加载 dump.rdb 文件恢复数据。

AOF:Redis 默认不开启。AOF采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改 Redis 数据的命令时,

就会把命令写入到AOF文件中。Redis重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作。

Redis将信息保存在内存

RDB:(Redis Database Backup)

数据库快照,(将当前数据库转换为二进制的数据保存在硬盘上),Redis生成一个dump.rdb的文件

我们可以在Redis安装程序的配置文件中进行配置

空白位置编写如下内容

save 60 5

60表示秒数,既1分钟

5表示key被修改的次数

配置效果:1分钟内如果有5个key以上被修改,就启动rdb数据库快照程序

优点:

因为是整体Redis数据的二进制格式,数据恢复是整体恢复的

缺点:

生成的rdb文件是一个硬盘上的文件,读写效率是较低的

如果突然断电,只能恢复最后一次生成的rdb中的数据

AOF(Append Only File):

AOF策略是将Redis运行过的所有命令(日志)备份下来

这样即使信息丢失,我们也可能根据运行过的日志,恢复为断电前的样子

它的配置如下

appendonly yes

特点:只保存命令不保存数据

理论上Redis运行过的命令都可以保存下来

但是实际情况下,Redis非常繁忙时,我们会将日志命令缓存之后,整体发送给备份,减少io次数以提高备份的性能和对Redis性能的影响

实际开发中,配置一般会采用每秒将日志文件发送一次的策略,断电最多丢失1秒数据

为了减少日志的大小

Redis支持AOF rewrite

将一些已经进行删除的数据的新增命令也从日志中移除,达到减少日志容量的目的

Redis拦截器

问题

开发中经常遇到因为某些原因导致接口重复提交,引发一系列的数据问题,因此在日常开发中必须规避这类的重复请求操作。

处理方式:

拦截器/AOP + Redis

处理思路:
在请求到达接口之前,判断当前用户是否在某个指定的时间周期内(比如5秒)已访问过此接口,如在时间内已访问此接口,则返回重复请求信息给用户。

​ 那么如何确定当前接口已被当前用户访问了呢?

将用户信息+接口url+客户端IP地址+参数等等(结合项目实际情况自定义,一个或多个不同组合,保证唯一性)存入到Redis中,设置过期时间,在这个时间未过期以前,将会对此用户此接口进行阻断操作。


Redis使用那些模式

1.主从复制

2**.哨兵模式**:是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的Master,并将所有Slave 连接到新的Master。所以整个运行哨兵的集群的数量不得少于3个节点。

3.Redis集群模式①数据分区:数据分区(或称数据分片) 是集群最核心的功能。

②高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似) ;当任一节点发生故障时,集群仍然可以对外提供服务。

Redis IO模型

多路复用IO模型:
1.多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。
2.在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
3.在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。
4.虽然可以采用多线程+ 阻塞IO 达到类似的效果,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。
5.而多路复用IO模式,通过一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO比较适合连接数比较多的情况。
6.另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
7.不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。


Redis过期key怎么处理,内存满了怎么处理

Redis过期key怎么处理方式

1.被动清理

当用户主动访问一个过期的key时,redis会将其直接从内存中删除。

2.主动清理

在内存主动清理的过程中,redis采用了一个随机算法来进行这个过程:简单来说,redis会随机的抽取N(默认100)个被设置了过期时间的key,检查这其中已经过期的key,将其清除。同时,如果其中已经过期的key超过了一定的百分比M(默认是25),则将继续执行一次主动清理,直至过期key的百分比在概率上降低到M以下。

在redis的持久化中,我们知道redis为了保持系统的稳定性,健壮性,会周期性的执行一个函数。在这个过程中,会进行之前已经提到过的自动的持久化操作,同时也会进行内存的主动清理。

3.内存不足时触发主动清理

在redis的内存不足时,也会触发主动清理。

内存满了怎么处理方式

缓存淘汰策略

Redis服务器繁忙时,有大量信息要保存

如果Redis服务器内存全满,再要往Redis中保存新的数据,就需要淘汰老数据,才能保存新数据

noeviction:返回错误**(默认)**

allkeys-random:所有数据中随机删除数据

volatile-random:有过期时间的数据库中随机删除数据

volatile-ttl:删除剩余有效时间最少的数据

allkeys-lru:所有数据中删除上次使用时间最久的数据

volatile-lru:有过期时间的数据中删除上次使用时间最久的数据

**allkeys-lfu:所有数据中删除使用频率最少的 **

volatile-lfu:有过期时间的数据中删除使用频率最少的


数据库

MyBatis的一级缓存和二级缓存

mybati一级缓存中的脏数据:

mybatis的一级缓存:默认是SqlSession级别,只要通过session查过的数据,都会放在session上,下一次再查询相同id的数据,都直接冲缓存中取出来,而不用到数据库里去取了。

mybatis一级缓存脏数据:当有不同的sqlSession在对数据库进行操作,一级缓存只能保证当前sqlSession中的增删改在一级缓存中自动更新,就会产生脏数据。

mybati二级缓存中的脏数据:

mybatis二级缓存:是SessionFactory级别,和namespace绑定,同一个namespace放到一个缓存对象中,当这个namaspace中执行了非select语句的时候,整个namespace中的缓存全部清除掉。

mybatis二级缓存脏数据:引起脏读的操作通常发生在多表关联操作中,比如在两个不同的mapper中都涉及到同一个表的增删改查操作,当其中一个mapper对这张表进行查询操作,此时另一个mapper进行了更新操作刷新缓存,然后第一个mapper又查询了一次,那么这次查询出的数据是脏数据。出现脏读的原因是他们的操作的缓存并不是同一个。

总结:所以不推荐使用mybatis的自带一二级缓存,推荐使用第三方缓存:memcached或者redis。

#{}和${}的区别

1.#{}是预编译处理,是占位符,${}是字符串替换,是拼接符

2.Mybatis在处理#{}的时候会将sql中的#{}替换成?号,调用PreparedStatement来赋值

3.Mybatis在处理 的时候就是把 {}的时候就是把 的时候就是把{}替换成变量的值,调用Statement来赋值

4.#{}的变量替换是在DBMS中、变量替换后,#{}对应的变量自动加上单引号

5. 的变量替换是在 D B M S 外、变量替换后, {}的变量替换是在DBMS外、变量替换后, 的变量替换是在DBMS外、变量替换后,{}对应的变量不会加上单引号

6.使用#{}可以有效的防止sql注入,提高系统的安全性

7.${}一般用在order by, limit, group by等场所。假设我们使用#{} 来指定order by字段,

比如:select * from student order by #{xCode},那么产生的SQL为select * from student order by ?, 
替换值后为select * from student order by ‘xCode’,Mybatis对xCode加了引号导致排序失败

一般${}用在我们能够确定值的地方,也就是我们程序员自己赋值的地方。
而#{}一般用在用户输入值的地方!!

数据库的三范式

第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项。

第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。

第三范式:第三范式是确保每列都和主键列直接相关,而不是间接相关。

数据库的四种隔离级别

1.读未提交(Read uncommitted):特点:事务可以读取到其他事务未提交/未回滚前的数据,会产生脏读
什么是脏读:由于事务读取到了其他事务未提交/未回滚前的数据,导致读取的数据最终是不存在的,这个现象就叫做脏读.

2.读已提交(Read committed):特点:事务只能读取到其他事务提交/回滚后的数据,解决了脏读问题,但是会产生不可重复读问题.

什么是不可重复读:在事务A执行期间,其他事务对事务A访问的数据进行修改操作,导致事务A中前后两次读取相同的数据的结果是不一致的.这个现象就叫做不可重复读

3.可重复读(Repeatable read):解决了不可重复读问题,产生了新的问题 – 幻读
什么是幻读: 在事务A访问数据期间,其他事务执行了插入操作,导致事务A前后两次读取到的数据总量不一致,这个现象就叫做幻读.

4.串行化(Serializable ):解决了幻读问题,实现了多事务并发执行同步效果,所以这个隔离级别的并发执行效率是最低下的。

以上四种隔离级别最高的是串行化级别,最低的是读未提交级别,当然级别越高,执行效率就越低。像串行化这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为可重复读。

在MySQL数据库中,支持上面四种隔离级别,默认为可重复读;而在 Oracle数据库 中,只支持串行化级别和读已提交,其中默认的为读已提交级别。

数据库锁概述

行锁和表锁

主要是针对锁粒度划分的,一般分为行锁、表锁、库锁

行锁:访问数据库的时候,锁定整个行数据,防止并发错误。

表锁:访问数据库的时候,锁定整个表数据,防止并发错误。

二者的区别:

表锁:开销小,加锁快,不会出现死锁;锁定粒度大,发生锁冲突概率高,并发度最低。

行锁:开销大,加锁慢,会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高。

乐观锁和悲观锁

**乐观锁:**顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有更新这个数据,可以使用版本号等机制。

乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是体用乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

**悲观锁:**顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 知道它拿到锁。

传统的关系型数据库里面就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。在Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。


如何优化数据库?

  • 使用读写分离,主从复制,集群,分库分表

如果主服务器宕机了,怎么办?

哨兵模式解决这个问题
哨兵系统中存在若干个哨兵实例,每个哨兵实例都会通过心跳机制与所有的服务器保持联系,每个一定的时间哨兵实例会向所有服务器发出pin命令,服务器接收到后会给出响应,若某个哨兵实例没有接收某台服务器得响应,则主观认为该服务器宕机,但是主观认为不代表客观宕机,此时需要确定是否真的宕机。
方式为:该哨兵实例会向其他的哨兵发出询问,若超过半数的哨兵都接收不到对应的响应,则客观认为服务器宕机,若宕机的是master,此时哨兵系统会从从服务器中选举一台作为新的master,将原来的master从集群中移除,并通知其他所有的slave,master发生了改变.让新的master与所有的slave重新建立联系.

慢sql优化

  1. 看慢sql是否使用了*,若是,则改为具体的字段

  2. 看慢sql是否使用了嵌套查询,此时是否可以将嵌套查询转换为联查,若可以,则使用联查,因为联查的效率高于嵌套

  3. 检查查询条件部分的字段是否需要使用索引,若需要,确定查询条件字段是否使用了索引,若没有,则添加索引

  4. 检查查询条件部分的字段是否添加了索引,若添加了索引,检查此时对字段进行条件查询的操作是否导致索引失效

    5.索引优化:在sql语句前面使用explain命令,查看mysql执行计划

img

sql语句没有走索引,排除没有建索引之外,最大的可能性是索引失效了。

索引的使用场景

  1. 表中的数据量大时,应该使用索引.表中数据量不大,不要使用索引,因为建立索引也是需要时间的.
  2. 通常会给作为查询条件的字段添加索引
  3. 当某字段的值会被频繁修改时,不要给该字段添加索引,因为每次修改都会改变元素的排序,从而导致索引重构,耗费时间
  4. 在一个表中,索引并不是越多越好,通常情况下,一个表中的索引不要超过6个

索引失效常见原因

索引失效是指:因为一些不当操作,导致进行全表扫描,而不使用索引,这种情况我们叫做索引失效。

使用索引时sql语句要避免的情况:
1.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
where name is not null /is null
2.应尽量避免在 where 子句中使用!=操作符,否则将引擎放弃使用索引而进行全表扫描
3.应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描
where name=xx or age=xx or col=xx 若其中一个字段没有索引,其他有索引的字段也不会走索引
4.not in 也要慎用,否则会导致全表扫描,in并不会导致索引失效
where …not in(xx,xx,xx)
适用in 会不会适用索引? – 会
5.尽量避免在where子句中对字段使用like左侧模糊查询(like ‘_%’),会导致全表扫描
where xx like ‘%xx’,like ’ _x’
6.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描
eg: select…from user where age+4>12
7.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描
eg:select…from …where round(score)=…

使用索引注意事项:
索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。

img

索引为什么能提高查询效率

  • 将读取到的数据块缓存到内存上后 ,对内存中缓存的数据块中的数据进行读取,采用的是二分查找算法
  • 索引使用B+Tree,在叶子数据块中保存的元素不是一个元素值,而是key-value

数据库安全:

SQL注入

通过预编译解决sql注入,使用#{}占位符进行占位,

如果SQL语句中存在变量,则必须使用PreparedStatement,解决SQL注入问题, 而且可以提高开发效率(避免了拼接字符串)

csrf攻击

csrf攻击解决方法:

目前,防御CSRF攻击主要有三种策略:

1.验证HTTP RefeWrer字段;

2.在请求地址中添加token并验证;

3.HTTP头中自定义属性并验证。

CSRF攻击原理:

CSRF跨站点请求伪造(Cross—Site Request Forgery),跟XSS攻击一样,存在巨大的危害性,你可以这样来理解:
攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

CSRF攻击实例
​ 受害者 Bob 在银行有一笔存款,通过对银行的网站发送请求 http://bank.example/withdraw?account=bob&amount=1000000&for=bob2 可以使 Bob 把 1000000 的存款转到 bob2 的账号下。通常情况下,该请求发送到网站后,服务器会先验证该请求是否来自一个合法的 session,并且该 session 的用户 Bob 已经成功登陆。

​ 黑客 Mallory 自己在该银行也有账户,他知道上文中的 URL 可以把钱进行转帐操作。Mallory 可以自己发送一个请求给银行:http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory。但是这个请求来自 Mallory 而非 Bob,他不能通过安全认证,因此该请求不会起作用。

​ 这时,Mallory 想到使用 CSRF 的攻击方式,他先自己做一个网站,在网站中放入如下代码: src=”http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory ”,并且通过广告等诱使 Bob 来访问他的网站。当 Bob 访问该网站时,上述 url 就会从 Bob 的浏览器发向银行,而这个请求会附带 Bob 浏览器中的 cookie 一起发向银行服务器。大多数情况下,该请求会失败,因为他要求 Bob 的认证信息。但是,如果 Bob 当时恰巧刚访问他的银行后不久,他的浏览器与银行网站之间的 session 尚未过期,浏览器的 cookie 之中含有 Bob 的认证信息。这时,悲剧发生了,这个 url 请求就会得到响应,钱将从 Bob 的账号转移到 Mallory 的账号,而 Bob 当时毫不知情。等以后 Bob 发现账户钱少了,即使他去银行查询日志,他也只能发现确实有一个来自于他本人的合法请求转移了资金,没有任何被攻击的痕迹。而 Mallory 则可以拿到钱后逍遥法外。

XSS攻击

XSS:跨站点脚本攻击,即CSS。利用网页开发时留下的漏洞(web应用程序对用户的输入过滤不足),巧妙的将恶意的代码注入到网页中,使用户浏览器加载并执行恶意制造的代码,以达到攻击的效果。这恶意的代码通常是JS代码,
但实际上也可以是JAVA、VBS、ActiveX、Flash或者是普通的HTML。
(浏览器不会判断,只要是符合解析,那么就会执行恶意的代码)。可能存在XSS的地方:微博、留言板、聊天室等收集用户输入的地方都可能遭受XSS攻击的风险。只要你对用户的输入没有严格过滤。

解决方法:

总原则:输入作过滤,输出作转义
过滤:根据业务需求进行过滤,比如输入点要求输入手机号,则只允许输入手机号格式的数字。
转义:所有输出到前端的数据都根据输出点进行转义,比如输出到html中进行html实体转义,输入到js里面的进行js转义。


#{}和${}的区别

1.#{}是预编译处理,是占位符,${}是字符串替换,是拼接符

2.Mybatis在处理#{}的时候会将sql中的#{}替换成?号,调用PreparedStatement来赋值

3.Mybatis在处理 的时候就是把 {}的时候就是把 的时候就是把{}替换成变量的值,调用Statement来赋值

4.#{}的变量替换是在DBMS中、变量替换后,#{}对应的变量自动加上单引号

5. 的变量替换是在 D B M S 外、变量替换后, {}的变量替换是在DBMS外、变量替换后, 的变量替换是在DBMS外、变量替换后,{}对应的变量不会加上单引号

6.使用#{}可以有效的防止sql注入,提高系统的安全性


JDBC连接数据库步骤

1.加载驱动
2.获取数据库连接
3.创建SQL语句对象
4.执行SQL语句
5.处理结果集
6.关闭连接


数据库事务

1.认识事务

1.1 为什么需要数据库事务

转账是生活中常见的操作,比如从A账户转账100元到B账号。站在用户角度而言,这是一个逻辑上的单一操作,然而在数据库系统中,至少会分成两个步骤来完成:

  • 1.将A账户的金额减少100元
  • 2.将B账户的金额增加100元。

img

在这个过程中可能会出现以下问题:

  • 1.转账操作的第一步执行成功,A账户上的钱减少了100元,但是第二步执行失败或者未执行便发生系统崩溃,导致B账户并没有相应增加100元。
  • 2.转账操作刚完成就发生系统崩溃,系统重启恢复时丢失了崩溃前的转账记录。
  • 3.同时又另一个用户转账给B账户,由于同时对B账户进行操作,导致B账户金额出现异常。

为了便于解决这些问题,需要引入数据库事务的概念。

1.2 什么是数据库事务

定义:数据库事务是构成单一逻辑工作单元的操作集合
一个典型的数据库事务如下所示

BEGIN TRANSACTION  //事务开始
SQL1
SQL2
COMMIT/ROLLBACK   //事务提交或回滚

关于事务的定义有几点需要解释下:

  • 1.数据库事务可以包含一个或多个数据库操作,但这些操作构成一个逻辑上的整体。
  • 2.构成逻辑整体的这些数据库操作,要么全部执行成功,要么全部不执行。
  • 3.构成事务的所有操作,要么全都对数据库产生影响,要么全都不产生影响,即不管事务是否执行成功,数据库总能保持一致性状态。
  • 4.以上即使在数据库出现故障以及并发事务存在的情况下依然成立。
1.3 事务如何解决问题

对于上面的转账例子,可以将转账相关的所有操作包含在一个事务中

BEGIN TRANSACTION 
A账户减少100元
B账户增加100元
COMMIT
  • 1.当数据库操作失败或者系统出现崩溃,系统能够以事务为边界进行恢复,不会出现A账户金额减少而B账户未增加的情况。
  • 2.当有多个用户同时操作数据库时,数据库能够以事务为单位进行并发控制,使多个用户对B账户的转账操作相互隔离。

事务使系统能够更方便的进行故障恢复以及并发控制,从而保证数据库状态的一致性。

1.4 事务的ACID特性及实现原理

原子性(Atomicity):事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。

一致性(Consistency):事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态。一致性状态是指:1.系统的状态满足数据的完整性约束(主码,参照完整性,check约束等) 2.系统的状态反应数据库本应描述的现实世界的真实状态,比如转账前后两个账户的金额总和应该保持不变。

隔离性(Isolation):并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。

持久性(Durability):事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。

在事务的ACID特性中,C即一致性是事务的根本追求,而对数据一致性的破坏主要来自两个方面

  • 1.事务的并发执行
  • 2.事务故障或系统故障

数据库系统是通过并发控制技术和日志恢复技术来避免这种情况发生的。

并发控制技术保证了事务的隔离性,使数据库的一致性状态不会因为并发执行的操作被破坏。
日志恢复技术保证了事务的原子性,使一致性状态不会因事务或系统故障被破坏。同时使已提交的对数据库的修改不会因系统崩溃而丢失,保证了事务的持久性。
img

2.并发异常与并发控制技术

2.1 常见的并发异常

在讲解并发控制技术前,先简单介绍下数据库常见的并发异常。

  • 脏写
    脏写是指事务回滚了其他事务对数据项的已提交修改,比如下面这种情况
    img

在事务1对数据A的回滚,导致事务2对A的已提交修改也被回滚了。

  • 丢失更新
    丢失更新是指事务覆盖了其他事务对数据的已提交修改,导致这些修改好像丢失了一样。
    img

事务1和事务2读取A的值都为10,事务2先将A加上10并提交修改,之后事务2将A减少10并提交修改,A的值最后为,导致事务2对A的修改好像丢失了一样

  • 脏读
    脏读是指一个事务读取了另一个事务未提交的数据
    img

在事务1对A的处理过程中,事务2读取了A的值,但之后事务1回滚,导致事务2读取的A是未提交的脏数据。

  • 不可重复读
    不可重复读是指一个事务对同一数据的读取结果前后不一致。脏读和不可重复读的区别在于:前者读取的是事务未提交的脏数据,后者读取的是事务已经提交的数据,只不过因为数据被其他事务修改过导致前后两次读取的结果不一样,比如下面这种情况
    img

由于事务2对A的已提交修改,事务1前后两次读取的结果不一致。

  • 幻读
    幻读是指事务读取某个范围的数据时,因为其他事务的操作导致前后两次读取的结果不一致。幻读和不可重复读的区别在于,不可重复读是针对确定的某一行数据而言,而幻读是针对不确定的多行数据。因而幻读通常出现在带有查询条件的范围查询中,比如下面这种情况:
    img

事务1查询A<5的数据,由于事务2插入了一条A=4的数据,导致事务1两次查询得到的结果不一样

2.2 事务的隔离级别
  1. 事务具有隔离性,理论上来说事务之间的执行不应该相互产生影响,其对数据库的影响应该和它们串行执行时一样。
  2. 然而完全的隔离性会导致系统并发性能很低,降低对资源的利用率,因而实际上对隔离性的要求会有所放宽,这也会一定程度造成对数据库一致性要求降低
  3. SQL标准为事务定义了不同的隔离级别,从低到高依次是
  • 读未提交(READ UNCOMMITTED)
  • 读已提交(READ COMMITTED)
  • 可重复读(REPEATABLE READ)
  • 串行化(SERIALIZABLE)

事务的隔离级别越低,可能出现的并发异常越多,但是通常而言系统能提供的并发能力越强。

不同的隔离级别与可能的并发异常的对应情况如下表所示,有一点需要强调,这种对应关系只是理论上的,对于特定的数据库实现不一定准确,比如mysql
的Innodb存储引擎通过Next-Key Locking技术在可重复读级别就消除了幻读的可能。
img

所有事务隔离级别都不允许出现脏写,而串行化可以避免所有可能出现的并发异常,但是会极大的降低系统的并发处理能力。

2.3 事务隔离性的实现—常见的并发控制技术

并发控制技术是实现事务隔离性以及不同隔离级别的关键,实现方式有很多,按照其对可能冲突的操作采取的不同策略可以分为乐观并发控制和悲观并发控制两大类。

  • 乐观并发控制:对于并发执行可能冲突的操作,假定其不会真的冲突,允许并发执行,直到真正发生冲突时才去解决冲突,比如让事务回滚。
  • 悲观并发控制:对于并发执行可能冲突的操作,假定其必定发生冲突,通过让事务等待(锁)或者中止(时间戳排序)的方式使并行的操作串行执行。

2.3.1 基于封锁的并发控制

核心思想:对于并发可能冲突的操作,比如读-写,写-读,写-写,通过锁使它们互斥执行。
锁通常分为共享锁和排他锁两种类型

  • 1.共享锁(S):事务T对数据A加共享锁,其他事务只能对A加共享锁但不能加排他锁。
  • 2.排他锁(X):事务T对数据A加排他锁,其他事务对A既不能加共享锁也不能加排他锁

基于锁的并发控制流程:

  1. 事务根据自己对数据项进行的操作类型申请相应的锁(读申请共享锁,写申请排他锁)
  2. 申请锁的请求被发送给锁管理器。锁管理器根据当前数据项是否已经有锁以及申请的和持有的锁是否冲突决定是否为该请求授予锁。
  3. 若锁被授予,则申请锁的事务可以继续执行;若被拒绝,则申请锁的事务将进行等待,直到锁被其他事务释放。

可能出现的问题:

  • 死锁:多个事务持有锁并互相循环等待其他事务的锁导致所有事务都无法继续执行。
  • 饥饿:数据项A一直被加共享锁,导致事务一直无法获取A的排他锁。

对于可能发生冲突的并发操作,锁使它们由并行变为串行执行,是一种悲观的并发控制。

2.3.2 基于时间戳的并发控制

核心思想:对于并发可能冲突的操作,基于时间戳排序规则选定某事务继续执行,其他事务回滚。

系统会在每个事务开始时赋予其一个时间戳,这个时间戳可以是系统时钟也可以是一个不断累加的计数器值,当事务回滚时会为其赋予一个新的时间戳,先开始的事务时间戳小于后开始事务的时间戳。

每一个数据项Q有两个时间戳相关的字段:
W-timestamp(Q):成功执行write(Q)的所有事务的最大时间戳
R-timestamp(Q):成功执行read(Q)的所有事务的最大时间戳

时间戳排序规则如下:

  1. 假设事务T发出read(Q),T的时间戳为TS
    a.若TS(T)<W-timestamp(Q),则T需要读入的Q已被覆盖。此
    read操作将被拒绝,T回滚。
    b.若TS(T)>=W-timestamp(Q),则执行read操作,同时把
    R-timestamp(Q)设置为TS(T)与R-timestamp(Q)中的最大值
  2. 假设事务T发出write(Q)
    a.若TS(T)<R-timestamp(Q),write操作被拒绝,T回滚。
    b.若TS(T)<W-timestamp(Q),则write操作被拒绝,T回滚。
    c.其他情况:系统执行write操作,将W-timestamp(Q)设置
    为TS(T)。

基于时间戳排序和基于锁实现的本质一样:对于可能冲突的并发操作,以串行的方式取代并发执行,因而它也是一种悲观并发控制。它们的区别主要有两点:

  • 基于锁是让冲突的事务进行等待,而基于时间戳排序是让冲突的事务回滚。
  • 基于锁冲突事务的执行次序是根据它们申请锁的顺序,先申请的先执行;而基于时间戳排序是根据特定的时间戳排序规则。

2.3.3 基于有效性检查的并发控制

核心思想:事务对数据的更新首先在自己的工作空间进行,等到要写回数据库时才进行有效性检查,对不符合要求的事务进行回滚。

基于有效性检查的事务执行过程会被分为三个阶段:

  1. 读阶段:数据项被读入并保存在事务的局部变量中。所有write操作都是对局部变量进行,并不对数据库进行真正的更新。
  2. 有效性检查阶段:对事务进行有效性检查,判断是否可以执行write操作而不违反可串行性。如果失败,则回滚该事务。
  3. 写阶段:事务已通过有效性检查,则将临时变量中的结果更新到数据库中。

有效性检查通常也是通过对事务的时间戳进行比较完成的,不过和基于时间戳排序的规则不一样。

该方法允许可能冲突的操作并发执行,因为每个事务操作的都是自己工作空间的局部变量,直到有效性检查阶段发现了冲突才回滚。因而这是一种乐观的并发策略。

2.3.4 基于快照隔离的并发控制

快照隔离是多版本并发控制(mvcc)的一种实现方式。

其核心思想是:数据库为每个数据项维护多个版本(快照),每个事务只对属于自己的私有快照进行更新,在事务真正提交前进行有效性检查,使得事务正常提交更新或者失败回滚。

由于快照隔离导致事务看不到其他事务对数据项的更新,为了避免出现丢失更新问题,可以采用以下两种方案避免:

  • 先提交者获胜:对于执行该检查的事务T,判断是否有其他事务已经将更新写入数据库,是则T回滚否则T正常提交。
  • 先更新者获胜:通过锁机制保证第一个获得锁的事务提交其更新,之后试图更新的事务中止。

事务间可能冲突的操作通过数据项的不同版本的快照相互隔离,到真正要写入数据库时才进行冲突检测。因而这也是一种乐观并发控制。

2.3.5 关于并发控制技术的总结

以上只是对常见的几种并发控制技术进行了介绍,不涉及特别复杂的原理的讲解。之所以这么做一是要真的把原理和实现细节讲清楚需要涉及的东西太多,篇幅太长,从作者和读者角度而言都不是一件轻松的事,所以只对其实现的核心思想和实现要点进行了简单的介绍,其他部分就一笔带过了。二是并发控制的实现的方式太过多样,基于封锁的实现就有很多变体,mvcc多版本并发控制的实现方式就更是多样,而且很多时候会和其他并发控制方式比如封锁的方式结合起来使用。

3. 故障与故障恢复技术

3.1 为什么需要故障恢复技术

数据库运行过程中可能会出现故障,这些故障包括事务故障和系统故障两大类

  • 事务故障:比如非法输入,系统出现死锁,导致事务无法继续执行。
  • 系统故障:比如由于软件漏洞或硬件错误导致系统崩溃或中止。

这些故障可能会对事务和数据库状态造成破坏,因而必须提供一种技术来对各种故障进行恢复,保证数据库一致性,事务的原子性以及持久性。数据库通常以日志的方式记录数据库的操作从而在故障时进行恢复,因而可以称之为日志恢复技术。

3.2 事务的执行过程以及可能产生的问题

img

事务的执行过程可以简化如下:

  1. 系统会为每个事务开辟一个私有工作区
  2. 事务读操作将从磁盘中拷贝数据项到工作区中,在执行写操作前所有的更新都作用于工作区中的拷贝.
  3. 事务的写操作将把数据输出到内存的缓冲区中,等到合适的时间再由缓冲区管理器将数据写入到磁盘。

由于数据库存在立即修改和延迟修改,所以在事务执行过程中可能存在以下情况:

  • 在事务提交前出现故障,但是事务对数据库的部分修改已经写入磁盘数据库中。这导致了事务的原子性被破坏。
  • 在系统崩溃前事务已经提交,但数据还在内存缓冲区中,没有写入磁盘。系统恢复时将丢失此次已提交的修改。这是对事务持久性的破坏。
3.3 日志的种类和格式
  • <T,X,V1,V2>:描述一次数据库写操作,T是执行写操作的事务的唯一标识,X是要写的数据项,V1是数据项的旧值,V2是数据项的新值。
  • <T,X,V1>:对数据库写操作的撤销操作,将事务T的X数据项恢复为旧值V1。在事务恢复阶段插入。
  • <T start>: 事务T开始
  • <T commit>: 事务T提交
  • <T abort>: 事务T中止

关于日志,有以下两条规则

  • 1.系统在对数据库进行修改前会在日志文件末尾追加相应的日志记录。
  • 2.当一个事务的commit日志记录写入到磁盘成功后,称这个事务已提交,但事务所做的修改可能并未写入磁盘
3.4 日志恢复的核心思想
  • 撤销事务undo:将事务更新的所有数据项恢复为日志中的旧值,事务撤销完毕时将插入一条<T abort>记录。
  • 重做事务redo:将事务更新的所有数据项恢复为日志中的新值。

事务正常回滚/因事务故障中止将进行redo
系统从崩溃中恢复时将先进行redo再进行undo。

以下事务将进行undo:日志中只包括<T start>记录,但既不包括<T commit>记录也不包括<T abort>记录.

以下事务将进行redo:日志中包括<T start>记录,也包括<T commit>记录或<T abort>记录。

假设系统从崩溃中恢复时日志记录如下

<T0 start>



<T0,A,1000,950>



<T0,B,2000,2050>



<T0 commit>



<T1 start>



<T1,C,700,600>



 

由于T0既有start记录又有commit记录,将会对事务T0进行重做,执行相应的redo操作。
由于T1只有start记录,将会对T1进行撤销,执行相应的undo操作,撤销完毕将写入一条abort记录。

3.5 事务故障中止/正常回滚的恢复流程
  1. 从后往前扫描日志,对于事务T的每个形如<T,X,V1,V2>的记录,将旧值V1写入数据项X中。
  2. 往日志中写一个特殊的只读记录<T,X,V1>,表示将数据项恢复成旧值V1,
    这是一个只读的补偿记录,不需要根据它进行undo。
  3. 一旦发现了<T start>日志记录,就停止继续扫描,并往日志中写一个
    <T abort>日志记录。
    img
3.6 系统崩溃时的恢复过程(带检查点)

检查点是形如<checkpoint L>的特殊的日志记录,L是写入检查点记录时还未提交的事务的集合,系统保证在检查点之前已经提交的事务对数据库的修改已经写入磁盘,不需要进行redo。检查点可以加快恢复的过程。

系统奔溃时的恢复过程分为两个阶段:重做阶段和撤销阶段。

重做阶段:

  1. 系统从最后一个检查点开始正向的扫描日志,将要重做的事务的列表undo-list设置为检查点日志记录中的L列表。
  2. 发现<T,X,V1,V2>的更新记录或<T,X,V>的补偿撤销记录,就重做该操作。
  3. 发现<T start>记录,就把T加入到undo-list中。
  4. 发现<T abort><T commit>记录,就把T从undo-list中去除。

撤销阶段:

  1. 系统从尾部开始反向扫描日志
  2. 发现属于undo-list中的事务的日志记录,就执行undo操作
  3. 发现undo-list中事务的T的<T start>记录,就写入一条<T abort>记录,
    并把T从undo-list中去除。

4.undo-list为空,则撤销阶段结束

总结:先将日志记录中所有事务的更新按顺序重做一遍,在针对需要撤销的事务按相反的顺序执行其更新操作的撤销操作。

3.6.1 一个系统崩溃恢复的例子

恢复前的日志如下,写入最后一条日志记录后系统崩溃

<T0 start>
<T0,B,2000,2050>
<T2 commit>
<T1 start>
<checkpoint {T0,T1}>   //之前T2已经commit,故不用重做
<T1,C,700,600>
<T1 commit>
<T2 start>
<T2,A,500,400>
<T0,B,2000>
<T0 abort>   //T0回滚完成,插入该记录后系统崩溃

img

4. 总结

事务是数据库系统进行并发控制的基本单位,是数据库系统进行故障恢复的基本单位,从而也是保持数据库状态一致性的基本单位。ACID是事务的基本特性,数据库系统是通过并发控制技术和日志恢复技术来对事务的ACID进行保证的,从而可以得到如下的关于数据库事务的概念体系结构。

img


消息中间件

消息队列的特征

  • 利用异步的特性,提高服务器的运行效率,减少因为远程调用出现的线程等待\阻塞
  • 削峰填谷:在并发峰值超过当前系统处理能力时,我们将没处理的信息保存在消息队列中,在后面出现的较闲的时间中去处理,直到所有数据依次处理完成,能够防止在并发峰值时短时间大量请求而导致的系统不稳定
  • 消息队列的延时:因为是异步执行,请求的发起者并不知道消息何时能处理完,如果业务不能接收这种延迟,就不要使用消息队列


幂等性

一、什么是幂等?

**幂等性:**多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。

二、使用幂等的场景

1、前端重复提交

用户注册,用户创建商品等操作,前端都会提交一些数据给后台服务,后台需要根据用户提交的数据在数据库中创建记录。如果用户不小心多点了几次,后端收到了好几次提交,这时就会在数据库中重复创建了多条记录。这就是接口没有幂等性带来的 bug。

2、接口超时重试

对于给第三方调用的接口,有可能会因为网络原因而调用失败,这时,一般在设计的时候会对接口调用加上失败重试的机制。如果第一次调用已经执行了一半时,发生了网络异常。这时再次调用时就会因为脏数据的存在而出现调用异常。

3、消息重复消费

在使用消息中间件来处理消息队列,且手动 ack 确认消息被正常消费时。如果消费者突然断开连接,那么已经执行了一半的消息会重新放回队列。

当消息被其他消费者重新消费时,如果没有幂等性,就会导致消息重复消费时结果异常,如数据库重复数据,数据库数据冲突,资源重复等。

三、解决方案

1、token 机制实现

通过token 机制实现接口的幂等性,这是一种比较通用性的实现方法。

示意图如下:

阿里面试官:接口的幂等性怎么设计?

具体流程步骤:

  1. 客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token 保存在 redis 中,同时把这个 ID 返回给客户端
  2. 客户端第二次调用业务请求的时候必须携带这个 token
  3. 服务端会校验这个 token,如果校验成功,则执行业务,并删除 redis 中的 token
  4. 如果校验失败,说明 redis 中已经没有对应的 token,则表示重复操作,直接返回指定的结果给客户端

注意:

  1. 对 redis 中是否存在 token 以及删除的代码逻辑建议用 Lua 脚本实现,保证原子性
  2. 全局唯一 ID 可以用百度的 uid-generator、美团的 Leaf 去生成

2、基于 mysql 实现

这种实现方式是利用 mysql 唯一索引的特性。

示意图如下:

阿里面试官:接口的幂等性怎么设计?

具体流程步骤:

  1. 建立一张去重表,其中某个字段需要建立唯一索引
  2. 客户端去请求服务端,服务端会将这次请求的一些信息插入这张去重表中
  3. 因为表中某个字段带有唯一索引,如果插入成功,证明表中没有这次请求的信息,则执行后续的业务逻辑
  4. 如果插入失败,则代表已经执行过当前请求,直接返回

3、基于 redis 实现

这种实现方式是基于 SETNX 命令实现的

SETNX key value:将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。

该命令在设置成功时返回 1,设置失败时返回 0。

示意图如下:

阿里面试官:接口的幂等性怎么设计?

具体流程步骤:

  1. 客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段
  2. 将该字段以 SETNX 的方式存入 redis 中,并根据业务设置相应的超时时间
  3. 如果设置成功,证明这是第一次请求,则执行后续的业务逻辑
  4. 如果设置失败,则代表已经执行过当前请求,直接返回

总结

这几种实现幂等的方式其实都是大同小异的,类似的还有使用状态机、悲观锁、乐观锁的方式来实现,都是比较简单的。

总之,当设计一个接口的时候,幂等都是首要考虑的问题,特别是当你负责设计转账、支付这种涉及到 money 的接口,你要格外注意!


maven常见命令

依赖冲突

举个例子,现在你的项目中,使用了两个Jar包,分别是A和B。现在A需要依赖另一个Jar包C,B也需要依赖C。但是A依赖的C的版本是1.0,B依赖的C的版本是2.0。这时候,Maven会将这1.0的C和2.0的C都下载到你的项目中,这样你的项目中就存在了不同版本的C,这时Maven会依据依赖路径最短优先原则,来决定使用哪个版本的Jar包,而另一个无用的Jar包则未被使用,这就是所谓的依赖冲突

mvn -v //查看版本 
mvn archetype:create //创建 Maven 项目 
mvn compile //编译源代码 
mvn test-compile //编译测试代码 
mvn test //运行应用程序中的单元测试 
mvn site //生成项目相关信息的网站 
mvn package //依据项目生成 jar 文件 
mvn install //在本地 Repository 中安装 jar 
mvn -Dmaven.test.skip=true //忽略测试文档编译 
mvn clean //清除目标目录中的生成结果 
mvn clean compile //将.java类编译为.class文件 
mvn clean package //进行打包 
mvn clean test //执行单元测试 
mvn clean deploy //部署到版本仓库 
mvn clean install //使其他项目使用这个jar,会安装到maven本地仓库中 
mvn archetype:generate //创建项目架构 
mvn dependency:list //查看已解析依赖 
mvn dependency:tree com.xx.xxx //看到依赖树 
mvn dependency:analyze //查看依赖的工具 
mvn help:system //从中央仓库下载文件至本地仓库 
mvn help:active-profiles //查看当前激活的profiles 
mvn help:all-profiles //查看所有profiles 
mvn help:effective -pom //查看完整的pom信息

Git命令

命令注释
git init初始化仓库,默认为 master 分支
git add .提交全部文件修改到缓存区
git rm --cached 文件名删除暂存区文件
git add 文件名提交某些文件到缓存区
git commit -m “<注释>” 文件名提交代码到本地仓库,并写提交注释(解决冲突时不要带文件名)
git reflog/git log查询版本精简日志/查看详细版本日志
git reset --hard 版本号git指针指向指定版本(**.git\refs\heads 本地查看当前指针指向版本)
git diff查看当前代码 add后,会 add 哪些内容
git diff --staged查看现在 commit 提交后,会提交哪些内容
git status查看当前分支状态
git pull <远程仓库名> <远程分支名>拉取远程仓库的分支与本地当前分支合并
git pull <远程仓库名> <远程分支名>:<本地分支名>拉取远程仓库的分支与本地某个分支合并
git commit -v提交时显示所有diff信息
git commit --amend [file1] [file2]重做上一次commit,并包括指定文件的新变化
git branch -v查看分支
git branch 分支名( git branch hot-fix)创建分支(创建热修分支)
git checkout 分支名切换分支
git merge 分支名把指定的分支合并到当前分支上
git remote add 别名 仓库地址创建别名
git remote -v查看别名

状态码

100(继续):请求者应当继续提出请求。服务器已收到请求的第一部分,正在等待剩余部分
101(切换协议):请求者要求服务器切换协议,服务器也已确认切换协议
200: 成功,请求数据通过响应报文的entity-body部分发送;OK
301: 表示这个网页已经永久的由服务器的A路径下移动到路径B302302 表示临时性重定向。访问一个Url时,被重定向到另一个url上。常用于页面跳转。
      与301的区别301是指永久性的移动,302是暂时性的,即以后还可能有变化
304: 客户端发出了条件式请求,但服务器上的资源未曾发生改变,则通过响应此响应状态码通知客户端;Not Modified
307:  浏览器内部重定向
401: 需要输入账号和密码认证方能访问资源;Unauthorized
403: 请求被禁止;Forbidden(跨域问题、路径错)
404: 服务器无法找到客户端请求的资源;Not Found
500: 服务器内部错误;Internal Server Error
502: 代理服务器从后端服务器收到了一条伪响应,如无法连接到网关;Bad Gateway
503: 服务不可用,临时服务器维护或过载,服务器无法处理请求
504: 网关超时

网络

幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用

场景1:支付场景
用户购买商品使用支付宝支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,
返回结果成功,用户查询余额返发现多扣钱了。因此需要对于每一笔订单,操作多次,也只能扣一次钱。

场景2:一键三连
小破站有一个一键三连的功能,长按可以对up主进行激励,每个人对每个视频只有一个一键三连的机会。就算再喜欢某个视频,多次操作,
也只能有一键三连一次。

场景3:统计DAU/MAU
DAU/MAU,又叫日活/月活,是用于反映网站、互联网应用或网络游戏的运营情况的统计指标。
所以一个用户当天或者当月登录多次(或者达到某种活跃用户判断机制多次),也只能看作一个活跃用户,不能重复计算。

HTTP 与 HTTPS 的区别

1、HTTPS 协议需要到 CA (Certificate Authority,证书颁发机构)申请证书,一般免费证书较少,

因而需要一定费用。(以前的网易官网是http,而网易邮箱是 https 。)

2、HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。

3、HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

4、HTTP 的连接很简单,是无状态的。HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。)


tcp 和 udp 的区别

tcp 和 udp 是 OSI 模型中的运输层中的协议。tcp 提供可靠的通信传输,而 udp 则常被用于让广播和细节控制交给应用的通信传输。

两者的区别大致如下:

tcp 面向连接,udp 面向非连接即发送数据前不需要建立链接;

tcp 提供可靠的服务(数据传输),udp 无法保证;

tcp 面向字节流,udp 面向报文;

tcp 数据传输慢,udp 数据传输快;


Request和Response

HTTP协议 超文本传输协议 由万维网制定(w3c)
是浏览器与服务器通讯的应用层协议,规定了浏览器与服务器之间的交互规则以及交互数据的格式信息等。

HTTP协议对于客户端与服务端之间的交互规则有以下定义:
要求浏览器与服务端之间必须遵循一问一答的规则,
即:浏览器与服务端建立TCP连接后需要先发送一个请求(问)然后服务端接收到请求并予以处理后再发送响应(答)。
注意,服务端永远不会主动给浏览器发送信息。

HTTP要求浏览器与服务端的传输层协议必须是可靠的传输,因此是使用TCP协议作为传输层协议的。

HTTP协议对于浏览器与服务端之间交互的数据格式,内容也有一定的要求。
浏览器给服务端发送的内容称为请求Request
服务端给浏览器发送的内容称为响应Response

请求和响应中大部分内容都是文本信息(字符串),并且这些文本数据使用的字符集为:
ISO8859-1.这是一个欧洲的字符集,里面是不支持中文的!!!。而实际上请求和响应出现的字符也就是英文,数字,符号。

HTTP请求Request

请求是浏览器发送给服务端的内容,HTTP协议中一个请求由三部分构成:
分别是:请求行,消息头,消息正文。消息正文部分可以没有。

1:请求行
请求行是一行字符串,以连续的两个字符(回车符和换行符)作为结束这一行的标志。
回车符:在ASC编码中2进制内容对应的整数是13.回车符通常用cr表示。
换行符:在ASC编码中2进制内容对应的整数是10.换行符通常用lf表示。
回车符和换行符实际上都是不可见字符。

请求行分为三部分:
请求方式(SP)抽象路径(SP)协议版本(CRLF) 注:SP是空格

GET /myweb/index.html HTTP/1.1
GET / HTTP/1.1

URL地址格式:
协议://主机地址信息/抽象路径

http://localhost:8088/TeduStore/index
GET /TeduStore/index.html HTTP/1.1

2:消息头
消息头是浏览器可以给服务端发送的一些附加信息,有的用来说明浏览器自身内容,有的
用来告知服务端交互细节,有的告知服务端消息正文详情等。

消息头由若干行组成,每行结束也是以CRLF标志。
每个消息头的格式为:消息头的名字(:SP)消息的值(CRLF)
消息头部分结束是以单独的(CRLF)标志。
例如:

Host: localhost:8088(CRLF)
Connection: keep-alive(CRLF)
Upgrade-Insecure-Requests: 1(CRLF)
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36(CRLF)
Sec-Fetch-User: ?1(CRLF)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9(CRLF)
Sec-Fetch-Site: none(CRLF)
Sec-Fetch-Mode: navigate(CRLF)
Accept-Encoding: gzip, deflate, br(CRLF)
Accept-Language: zh-CN,zh;q=0.9(CRLF)(CRLF)

3:消息正文
消息正文是2进制数据,通常是用户上传的信息,比如:在页面输入的注册信息,上传的
附件等内容。

GET /myweb/reg?username=xxx&password=xxx HTTP/1.1
Host: localhost:8088
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
Sec-Fetch-User: ?1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
1010101101001.....

HTTP响应Response

响应是服务端发送给客户端的内容。一个响应包含三部分:状态行,响应头,响应正文

1:状态行
状态行是一行字符串(CRLF结尾),并且状态行由三部分组成,格式为:
protocol(SP)statusCode(SP)statusReason(CRLF)
协议版本(SP)状态代码(SP)状态描述(CRLF)
例如:
HTTP/1.1 200 OK

状态代码是一个3位数字,分为5类:
1xx:保留
2xx:成功,表示处理成功,并正常响应
3xx:重定向,表示处理成功,但是需要浏览器进一步请求
4xx:客户端错误,表示客户端请求错误导致服务端无法处理
5xx:服务端错误,表示服务端处理请求过程出现了错误

具体的数字在HTTP协议手册中有相关的定义,可参阅。
状态描述手册中根据不同的状态代码有参考值,也可以自行定义。通常使用参考值即可。

2.响应头:
响应头与请求中的消息头格式一致,表示的是服务端发送给客户端的附加信息。

3.响应正文:
2进制数据部分,包含的通常是客户端实际请求的资源内容。

响应的大致内容:

HTTP/1.1 404 NotFound(CRLF)
Content-Type: text/html(CRLF)
Content-Length: 2546(CRLF)(CRLF)
1011101010101010101......

这里的两个响应头:
Content-Type是用来告知浏览器响应正文中的内容是什么类型的数据(图片,页面等等)
不同的类型对应的值是不同的,比如:

文件类型Content-Type对应的值
htmltext/html
csstext/css
jsapplication/javascript
gifimage/gif
jpgimage/jpeg

Content-Length是用来告知浏览器响应正文的长度,单位是字节。

浏览器接收正文前会根据上述两个响应头来得知长度和类型从而读取出来做对应的处理以
显示给用户看。

Linux

downloads,softwares,programs三个文件夹,第一个存放下载文件,第二个存放安装的工具,第三个存放要部署的项目

查看日志的命令

1、tail命令,例“tail -n +10 test.log”查询10行之后的所有日志;

2、head命令,例“head -n 10 test.log”查询日志文件中的头10行日志;

3、cat命令;

查看进程的命令

1、PS命令,该命令可以查看哪些进程正在运行及其运行状态;

ps命令是一个相当强大地Linux进程查看命令.运用该命令可以确定有哪些进程正在运行和运行地状态、 进程是否结束、进程有没有僵死、哪些进程占用了过多地资源等等.总之大部分信息均为可以通过执行该命令得到地。

2、Top命令,该命令可以实时显示各个线程情况;

top命令可以实时显示各个线程情况。

3、Pstree命令,该命令以树状图的方式展现进程之间的派生关系;

pstree命令以树状图的方式展现进程之间的派生关系,显示效果比较直观。

4、Pgrep命令等等。

pgrep命令以名称为依据从运行进程队列中查找进程,并显示查找到的进程id。


系统运行多少时间

uptime

会告诉你系统运行了多长时间,会用一行显示信息,当前时间、系统运行时间、当前登录用户的数量、过去1分钟/5分钟/15分钟系统负载的均值。

# uptime
08:34:29 up 21 days, 5:46, 1 user, load average: 0.06, 0.04, 0.00

who命令

列出当前登录进计算机的用户。who命令与w命令类似,但后者还包含额外的数据和统计信息。

# who -b
system boot 2018-04-12 02:48

集群与分布式

集群与分布式的区别

分布式:把一个大业务拆分成多个子业务,每个子业务都是一套独立的系统,子业务之间相互协作最终完成整体的大业务。

集群:把处理同一个业务的系统部署多个节点 。

把一套系统拆分成不同的子系统部署在不同服务器上,这叫分布式。

**把多个相同的系统部署在不同的服务器上,这叫集群。**部署在不同服务器上的相同系统必然要做“负载均衡”。

集群主要是简单加机器解决问题,对于问题本身不做任何分解。

分布式处理里必然涉及任务分解与答案归并。分布式中的某个子任务节点,可以是一个集群,

该集群中的任一节点都作为一个完整的任务出现。

集群和分布式都是由多个节点组成,但集群中各节点间基本不需要通信协调,而分布式中各个节点的通信协调是必不可少的。
img

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

波 多

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值