Java面经

文章目录

  • 项目问题
    • 数据回流
      • 架构
      • 数据流图
      • 设计总结
      • 成果
    • 你这个项目的QPS、订单量有多少
    • 如何解决超卖问题
    • 假如让你设计一个秒杀系统,怎么设计才能承受百万级并发
      • 秒杀系统场景特点
      • 秒杀架构设计理念
      • 架构方案
      • 设计思路
        • 前端方案
          • 浏览器端(js):
        • 后端方案
          • 服务端控制器层(网关层)
          • 服务层
          • 数据库层
    • 讲讲数据库表怎么设计的
    • 为什么使用自增字段作主键
    • spring里面的控制反转,IoC
    • 高频搜索关键词,每个文件1G,每个文件有多行 TopK问题
    • 一个列表,找出最长的字串,满足字串的每个元素相差不超过limit
    • jwt优缺点
      • 优点
      • 缺点
      • 使用场景
    • 虚函数是什么
  • 逻辑题
    • 烧绳子
    • 1000瓶药水找毒药
    • 抢30
    • 灯泡开关
    • 圆环回原点问题
    • 扔鸡蛋问题
    • 切两刀构成三角形的概率
  • 操作系统
    • 交换机有哪些消息转发模式
    • 进程与线程 为什么要有进程和线程? 区别是什么?
    • 了解死锁吗? 如何避免如何预防
      • 死锁的定义
      • 死锁产生的原因
      • 死锁产生的必要条件
      • 如何避免死锁
        • 银行家算法
        • 加锁顺序
        • 加锁时限
        • 死锁检测
    • 死锁的解决措施
    • 线程间怎么通信的?
      • 互斥锁
      • 读写锁
      • 条件变量
      • 信号量机制(Semaphore)
      • 信号机制(Signal)
    • 进程间通信的方式
      • 管道( pipe )
      • 有名管道 (named pipe)
      • 信号 (sinal )
      • 信号量(semophore )
      • 消息队列( message queue )
      • 共享内存(shared memory )
      • 套接字(socket )
    • 消息队列和共享内存的区别?使用场景?
    • 虚拟内存
    • Kill工作原理
    • Linux的硬链接与软链接
  • SQL
    • 分库分表怎么设计
      • 分表
      • 分库
      • 分库分表
      • 分表策略
        • 纵向分表
        • 横向分表(水平分表)
    • 分库分表带来的问题
      • 1、事务一致性问题
        • 分布式事务
        • 最终一致性
      • 2、跨节点关联查询 join 问题
        • 1)全局表
        • 2)字段冗余
        • 3)数据组装
        • 4)ER分片
      • 3、跨节点分页、排序、函数问题
      • 4、全局主键避重问题
        • 1)UUID
        • 2)结合数据库维护主键ID表
        • 3)Snowflake分布式自增ID算法
      • 5、数据迁移、扩容问题
    • 水平分表依据什么分?时间还是数据
    • 分库分表之后我想查询近期的订单,怎么查
      • 1)可以通过Elasticsearch来构建二级索引
      • 2)直接把全量数据存储在ES中的方式来处理分库分表之后的多条件查询以及JOIN查询
    • 数据存储引擎有哪些
    • InnoDB和MylSAM的区别
    • select count(*)时InnoDB和MylSAM分别是怎么处理的
    • 聚簇索引和非聚簇索引的区别
    • 假如数据库某个字段是String类型,读的时候用int类型去接收会有什么问题?反过来呢?
    • 事务有哪些隔离级别,分别解决了什么问题
    • 可重复读是怎么解决脏读的
    • 有俩事务,事务A:读、写(张三)、读 事务B:读、写(李四)、读,假如俩事务同时执行,结果如何
    • 索引的结构? b+树为什么比b树矮胖?
    • SQL语句题 学生表 搜索各班级分数最高的学生
    • 为什么innodb要选择B+树
    • 乐观锁和悲观锁
      • 悲观锁(Pessimistic Lock)
      • 乐观锁(Optimistic Locking)
      • CAS带来的问题
        • ABA问题
        • 自旋循环时间消耗
        • CAS只能单变量
      • CAS底层
      • CAS 性能优化
      • 如何选择
    • MVCC
      • 基本原理
      • 基本特征
      • InnoDB存储引擎MVCC的实现策略
      • MVCC下InnoDB的增删查改是怎么work的
        • 1. 插入
        • 2、更新
        • 3、删除
        • 4、查询
      • 读操作的分类
    • innodb 有哪几种锁?
      • 共享/排它锁(Shared and Exclusive Locks)
      • 意向锁(Intention Locks)
      • 记录锁(Record Locks)
      • 间隙锁(Gap Locks)
      • 临键锁(Next-key Locks)
      • 插入意向锁(Insert Intention Locks)
      • 自增锁(Auto-inc Locks)
    • 什么时候表锁?什么时候行锁?
    • 为什么有 gap lock?
    • Mysql执行解析过程
      • 连接
      • 查询缓存
      • 语法解析和预处理
      • 查询优化(Query Optimizer)与查询执行计划
        • MySQL的优化器能处理哪些优化类型呢?
      • 存储引擎
      • 执行引擎(Execution Engine)
    • Mysql语句执行顺序
    • 视图
    • 日志
      • 事务是如何通过日志来实现的?
        • redo log
        • undo log
        • bin log
    • Mysql的主从复制
      • 好处
      • 原理
  • 计算机网络
    • OSI七层网络
    • tcp 三次握手过程以及为什么要三次握手
    • 第三次握手 ACK 丢失会发生什么
    • tcp 四次挥手过程第四次握手完是直接关闭连接吗为什么进入 time-wait
    • tcp 拥塞控制的方法
      • 慢开始
      • 拥塞避免(按线性规律增长)
      • 快重传
      • 快恢复(与快重传配合使用)
    • 知道哪些 http 状态码
    • HTTPS工作原理
    • 服务端需要输入一个数字,客户端输入了一个字符串,返回什么状态码?
    • http头部?
    • get和post的区别
      • 1、url可见性:
      • 2、数据传输上:
      • 3、缓存性:
      • 4、后退页面的反应
      • 5、传输数据的大小
      • 6、安全性
    • http为什么用到tcp,tcp怎么做到可靠?
      • 校验和
      • 确认应答与序列号
      • 超时重传
      • 连接管理
      • 流量控制
      • 拥塞控制
    • 拥塞控制与流量控制的区别
    • TCP拆包粘包
    • udp怎么实现可靠传输?
    • TCP UDP深挖 场景使用 特定场景下的选择
    • DNS解析过程
    • 一个URL到页面加载全过程
      • 1、DNS解析
      • 2、建立TCP连接
      • 3、发送HTTP请求
        • 请求行
        • 请求报头
        • 请求正文
      • 4、接收处理请求,服务器进行处理并返回HTTP报文
        • 状态码
        • 响应报头
        • 响应报文
      • 5、浏览器解析并渲染页面
      • 6、关闭连接
    • http协议,还说了个自定制,讨论了好久的自定制协议
    • http和https的区别
      • https的设计目标
      • 区别
      • https协议的改进
      • https优点
      • https缺点
      • HTTPS连接过程
    • 如何证明浏览器收到的公钥一定是该网站的公钥?
    • http使用的方法
    • Post是2个TCP,为什么比Get多一个TCP
    • 对称加密和非对称加密的实际使用场景,并判断经典使用场景
  • Java
    • 继承和多态
    • 重写和重载
      • 对多态性的体现不同
      • 规则不同
    • String、StringBuffer、StringBuilder
    • Java和C++的区别
    • ==与equals
    • hashCode与equals
      • 为什么重写equals时必须重写hashCode方法?
    • 类加载机制
      • 加载
      • 验证
      • 准备(重点)
      • 解析
      • 初始化(重点)
      • 使用
      • 卸载
    • JVM内存模型
      • 指令重排
      • 内存屏障
      • happen-before
    • 双亲委派机制
    • 类加载详细过程
    • JVM
      • 核心参数
      • 每个部分主要存放的内容
    • 垃圾回收机制
      • 垃圾判断方法
        • 引用计数法
        • 可达性分析算法
          • 虚拟机栈中引用的对象
          • 方法区中类静态属性引用的对象
          • 方法区中常量引用的对象
          • 本地方法栈中`JNI`引用的对象
      • 强引用,软引用,弱引用,虚引用
        • 强引用
        • 软引用
        • 弱引用
        • 虚引用
      • 垃圾回收算法
        • 标记-清除算法
        • 复制算法
        • 标记-整理算法
        • 分代收集算法
      • 回收策略
      • 常见的垃圾收集器
        • Serial收集器(复制算法)
        • Serial Old收集器(标记-整理算法)
        • ParNew收集器(复制算法)
        • Parallel Scavenge收集器(复制算法)
        • Parallel Old收集器(复制算法)
        • CMS(Concurrent Mark Sweep)收集器(标记-清除算法)
        • G1收集器(标记-整理算法)
      • JVM GC核心参数
      • 调优策略
    • 打印线程栈信息
    • 原子类了解吗? Cas
      • 作用
      • CAS(Compare And Swap)
        • Unsafe
    • 反射的底层实现?
      • 含义
      • 实现方式
      • 实现反射的类
      • 优缺点
    • select,poll,epoll的区别
    • 对象头
    • 多线程
      • 线程与进程的状态
      • 并发、并行
      • 什么是上下文切换?
      • 守护线程和用户线程有什么区别呢?
      • 创建线程的四种方式
      • 说一下 runnable 和 callable 有什么区别
      • 线程的 run()和 start()有什么区别
      • 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
      • 什么是 Callable 和 Future?
      • 什么是 FutureTask
      • 线程的状态
      • Java 中用到的线程调度算法是什么
      • sleep() 和 wait() 有什么区别
      • 为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里
      • 为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用
      • Thread 类中的 yield 方法有什么作用
      • 线程的 sleep()方法和 yield()方法有什么区别
      • 如何停止一个正在运行的线程
      • Java 中 interrupted 和 isInterrupted 方法的区别?
      • Java 如何实现多线程之间的通讯和协作
      • 如果你提交任务时,线程池队列已满,这时会发生什么
      • 什么是线程同步和线程互斥,有哪几种实现方式?
      • 线程之间如何通信及线程之间如何同步
      • Synchronized
        • 关键字的使用
        • 底层实现原理
        • 可重入的原理
        • 自旋
        • synchronized 锁升级的原理
      • 线程 B 怎么知道线程 A 修改了变量
      • 当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的synchronized 方法 B
      • synchronized 和 Lock 的区别
      • synchronized 和 ReentrantLock 区别
      • Java 中能创建 volatile 数组吗
      • synchronized 和 volatile 的区别
      • 线程池
        • 构造的核心参数
        • Executors框架实现的线程池/线程池的四种创建方式
        • 四种线程池的特点
        • 线程池的状态
        • 饱和策略(handler)
        • 执行原理
    • 集合
      • List、Map、Set关系
      • fail-fast
      • ArrayList扩容机制
      • HashMap的底层实现
        • **红黑树**
        • PUT的具体操作
        • 扩容的操作
        • Hash函数
        • HashMap长度为2的幂次方
        • 多线程操作导致死循环
      • ConcurrentHashMap线程安全的具体实现/底层具体实现
      • 迭代器 Iterator
      • Iterator 和 ListIterator 有什么区别?
      • 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?
    • 设计模式
      • 六大原则
      • 单例模式
        • 使用场景
        • 双重检验锁方式实现单例模式
        • 饿汉式-静态常量(线程安全)
        • 饿汉式-静态代码块(线程安全)
        • 懒汉式(线程不安全)
        • 懒汉式(线程安全,方法上加同步锁)
        • 双重校验锁(线程安全,效率高)
        • 静态内部类实现单例(线程安全、效率高)
      • 工厂模式
        • 使用场景
        • 简单工厂模式
        • 工厂方法模式
        • 抽象工厂模式
      • 代理模式
        • 使用场景
        • 静态代理
        • 动态代理
        • Cglib代理
      • 观察者模式
      • 观察者模式应用场景
  • 中间件
    • redis
      • Redis也扛不住了,万级流量会打到DB上,该怎么处理
      • Redis有哪5种数据类型及使用场景
        • String(简单操作)
        • hash(结构化数据)
        • list(有序)
        • set(并交集、是否存在)
        • zset (排序)
      • Redis是怎么删除过期key的
      • Redis有哪几种数据淘汰策略
      • Redis有哪些持久化方式
      • 项目中有用到吗?购物车用的什么数据类型? 为什么要用呢?
      • redis宕机缓存一致性?
        • 不一致的原因
        • 优化思路
      • 怎么判断某个节点的主机是否可以正常工作
      • 一致性哈希算法
      • 哈希槽个数为什么是16384个
      • redis主从复制
        • 全量复制
        • 增量复制
        • Redis主从同步策略
      • Redis 适合场景
      • 缓存击穿
      • 缓存雪崩
      • 缓存穿透
      • 布隆过滤器
      • 分布式锁
    • kafka
      • 为什么要使用
      • broker作用
      • kafka中的 zookeeper 起到什么作用
      • follower如何与leader同步数据
      • producer如何优化打入速度
      • Kafka中的消息是否会丢失和重复消费
      • kafka如何实现延迟队列
    • ES
      • 项目中为什么用到
      • 底层原理 (倒排索引,存储结构,压缩技巧)
    • MongoDB
      • 分片(sharding)和复制(replication)是怎样工作的
      • 数据在什么时候才会扩展到多个分片(shard)里
      • 当我试图更新一个正在被迁移的块(chunk)上的文档时会发生什么
      • 如果在一个分片(shard)停止或者很慢的时候,我发起一个查询会怎样
      • 我可以把 moveChunk 目录里的旧文件删除吗
      • 我怎么查看 Mongo 正在使用的链接
      • 如果块移动操作(moveChunk)失败了,我需要手动清除部分转移的文档吗
      • 如果我在使用复制技术(replication),可以一部分使用日志(journaling)而其他部分则不使用吗
      • 当更新一个正在被迁移的块(Chunk)上的文档时会发生什么
      • MongoDB 在 A:{B,C}上建立索引,查询 A:{B,C}和 A:{C,B}都会使用索引吗
      • MongoDB 支持存储过程吗?如果支持的话,怎么用
      • 如何理解 MongoDB 中的 GridFS 机制,MongoDB 为何使用 GridFS 来存储文件
  • Linux
      • linux如何查看端口是否被使用
      • Linux复杂的查找命令
      • 追加重定向和清空重定向的区别(> , >>)
  • Spring
    • Spring中重要的模块
    • IOC
    • AOP
    • Bean的作用域
    • spring 中Bean的生命周期
    • Spring框架中用到的设计模式
    • Spring MVC的运行流程
    • Spring MVC的主要组件
      • SpringBoot的特征?
    • Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
    • Spring Boot 配置加载顺序?
    • Spring Boot 实现全局异常处理
    • Spring Boot 中的监视器
      • Spring Boot 中如何解决跨域问题
      • SpringBoot读取配置相关注解
      • SpringBoot异常处理相关注解
      • Spring Boot 中如何实现定时任务 ?
      • Spring Boot 中的 starter 到底是什么 ?

项目问题

JVM调优

多线程实现(线程数是否是定死的, )

redis使用的结构(底层)、分布式锁、

设计模式 保证多线程下单例模式的准确性

spring aop的应用

数据回流

https://wiki.zhiyinlou.com/pages/viewpage.action?pageId=37034419

架构

数据流图

设计总结

成果

你这个项目的QPS、订单量有多少

如何解决超卖问题

数据库层面:

  1. 排它锁
 update goods set num = num - 1 WHERE id = 1001 and num > 0

排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。

就是类似于我在执行update操作的时候,这一行是一个事务**(默认加了排他锁**)。这一行不能被任何其他线程修改和读写

  1. CAS

    1 select version from goods WHERE id= 1001
    2 update goods set num = num - 1, version = version + 1 WHERE id= 1001 AND num > 0 AND version = @version(上面查到的version);
    
  2. redis单线程

    利用redis的单线程预减库存。比如商品有100件。那么我在redis存储一个k,v。例如

    每一个用户线程进来,key值就减1,等减到0的时候,全部拒绝剩下的请求。

    那么也就是只有100个线程会进入到后续操作

超卖即“超卖缺货”,当宝贝库存接近0时,如果多个买家同时付款购买此宝贝,将会出现“超卖缺货”现象。产生超卖缺货这种情况是商家无法控制的,并且发生这种情况的概率极低。 即**售出数量多于库存数量 **

解决超卖问题的方案:采用乐观锁。 
1,秒杀由于存在的广泛的用户,并发暴涨的情况下,一定要注意在
文案方面做得尽量人性话些。在系统承载不足的情况,不能让会员觉得被欺骗了。同时对于一些重要信息需要做到可查,比如显示订单的下单信息,付款信息等。 
2,在秒杀的时候,由于瞬时访问量导致应用的压力暴涨,数据库的load上升,IC(商品中心)的压力很大,从而导致了其他非秒杀的交易也受到了影响。

解决方案:

  1. 增加应用的机器

  2. 将秒杀应用与普通交易相隔离。对IC做了分组隔离,从而保证秒杀不会影响主站的其他交易。

  3. 由于商品详情页面(detail)该页面用户的刷新频率很高,所以 尽量将该页面静态化,淘宝的秒杀商品详情页面,去除了很多不必要的后台查询逻辑,比如卖家的信誉,星级等信息。

  4. detail页面的响应时间在3-5秒,主要原因是需要到数据库查询库存信息,该操作所花时间比较长,对数据库的压力也很大。所以采用了从缓存取库存信息。淘宝有一个tair缓存,在应用起来的时候,会将商品的库存信息加载到tair中。

  5. 聚划算的一次秒杀活动中,出现超卖的情况,原因是:库存信息是从tair中取的,拍下时在tair中减少了库存,但是在真正购买时,会去更新数据库中的库存,这样就导致数据库的当前库存信息又去更新了tair中的库存信息。

假如让你设计一个秒杀系统,怎么设计才能承受百万级并发

秒杀系统场景特点

  • 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
  • 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
  • 秒杀业务流程比较简单,一般就是下订单减库存。

秒杀架构设计理念

限流: 鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。

**削峰:**对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。

**异步处理:**秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。

**内存缓存:**秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。

**可拓展:**当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。

架构方案

Java面经_第1张图片

设计思路

**将请求拦截在系统上游,降低下游压力:**秒杀系统特点是并发量极大,但实际秒杀成功的请求数量却很少,所以如果不在前端拦截很可能造成数据库读写锁冲突,甚至导致死锁,最终请求超时。
**充分利用缓存:**利用缓存可极大提高系统读写速度。
**消息队列:**消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。

前端方案

浏览器端(js):

**页面静态化:**将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。
**禁止重复提交:**用户提交之后按钮置灰,禁止重复提交
**用户限流:**在某一时间段内只允许用户提交一次请求,比如可以采取IP限流

后端方案

服务端控制器层(网关层)

**限制uid(UserID)访问频率:**我们上面拦截了浏览器访问的请求,但针对某些恶意攻击或其它插件,在服务端控制层需要针对同一个访问uid,限制访问频率。

服务层

上面只拦截了一部分访问请求,当秒杀的用户量很大时,即使每个用户只有一个请求,到服务层的请求数量还是很大。比如我们有100W用户同时抢100台手机,服务层并发请求压力至少为100W。

  1. **采用消息队列缓存请求:**既然服务层知道库存只有100台手机,那完全没有必要把100W个请求都传递到数据库啊,那么可以先把这些请求都写到消息队列缓存一下,数据库层订阅消息减库存,减库存成功的请求返回秒杀成功,失败的返回秒杀结束。
  2. **利用缓存应对读请求:**对类似于12306等购票业务,是典型的读多写少业务,大部分请求是查询请求,所以可以利用缓存分担数据库压力。
  3. **利用缓存应对写请求:**缓存也是可以应对写请求的,比如我们就可以把数据库中的库存数据转移到Redis缓存中,所有减库存操作都在Redis中进行,然后再通过后台进程把Redis中的用户秒杀请求同步到数据库中。
数据库层

数据库层是最脆弱的一层,一般在应用设计时在上游就需要把请求拦截掉,数据库层只承担“能力范围内”的访问请求。所以,上面通过在服务层引入队列和缓存,让最底层的数据库高枕无忧

讲讲数据库表怎么设计的

为什么使用自增字段作主键

spring里面的控制反转,IoC

高频搜索关键词,每个文件1G,每个文件有多行 TopK问题

一个列表,找出最长的字串,满足字串的每个元素相差不超过limit

jwt优缺点

https://www.jianshu.com/p/576dbf44b2ae

基于session和基于jwt的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而jwt是保存在客户端的。

优点

  1. 可扩展性好 应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而jwt不需要。
  2. 无状态 jwt不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外jwt的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。

缺点

  1. 安全性

由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。

  1. 性能

jwt太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用jwt的http请求比使用session的开销大得多。

  1. 一次性

无状态是jwt的特点,但也导致了这个问题,jwt是一次性的。想修改里面的内容,就必须签发一个新的jwt。

(1)无法废弃 通过上面jwt的验证机制可以看出来,一旦签发一个jwt,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的JWT还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的jwt,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。

(2)续签 如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变jwt的有效时间,就要签发新的jwt。最简单的一种方式是每次请求刷新jwt,即每个http请求都返回一个新的jwt。这个方法不仅暴力不优雅,而且每次请求都要做jwt的加密解密,会带来性能问题。另一种方法是在redis中单独为每个jwt设置过期时间,每次访问时刷新jwt的过期时间。

可以看出想要破解jwt一次性的特性,就需要在服务端存储jwt的状态。但是引入 redis 之后,就把无状态的jwt硬生生变成了有状态了,违背了jwt的初衷。而且这个方案和session都差不多了。

使用场景

  • 有效期短
  • 只希望被使用一次

比如,用户注册后发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活),不能被篡改以激活其他可能的账户,一次性的。这种场景就适合使用jwt。

而由于jwt具有一次性的特性。单点登录和会话管理非常不适合用jwt,如果在服务端部署额外的逻辑存储jwt的状态,那还不如使用session。基于session有很多成熟的框架可以开箱即用,但是用jwt还要自己实现逻辑。

虚函数是什么

那些被virtual关键字修饰的成员函数,就是虚函数。

首先:强调一个概念定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。定义一个函数为纯虚函数,才代表函数没有被实现。定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。 虚函数只能借助于指针或者引用来达到多态的效果。

逻辑题

https://wenku.baidu.com/view/663726336137ee06eef91828.html

烧绳子

烧一根不均匀的绳,从头烧到尾总共需要1个小时。现在有若干条材质相同的绳子,问如何用烧绳的方法来计时一个小时十五分钟呢?

A,B,C三绳
A,C一头烧,B两头烧
B烧完时半小时,此时掐断C,A继续烧,A烧完时一小时,然后此时从两头烧C,烧完时时间为一小时十五分钟

A,B,C三绳

A两头烧,B一头烧,

当A烧完半个小时过去,这时B两头一块烧,烧完就是45分钟

1000瓶药水找毒药

一共 1000 瓶药水,其中 1 瓶有毒药。
已知小白鼠喝毒药一天内死
若想在一天内找到毒药,最少需要几只小白鼠?

https://codetop.cc/discuss/2

10只,二进制思想,一瓶药水可以给多个小白鼠喝,最后看死的哪几只

抢30

答案:尽量喊3的倍数

然后就会发现,这个游戏,谁先喊,谁一定输

Nim博弈游戏

灯泡开关

一个圆环上有 100 个灯泡,灯泡有亮和暗两种状态。按一个灯泡的开关可以改变它和与它相邻两个灯泡的状态。 设计一种算法,对于任意初始状态,使所有灯泡全亮。

https://codetop.cc/discuss/12

将灯泡编号 1 ~ 100

步骤一:将灯泡变为全亮或只剩一个为暗

从 1 循环到 98 ,遇到暗的则按它下一个,使之变亮。循环完毕,1 ~ 98 必然全亮。99 和 100可能为亮亮、暗亮、亮暗、暗暗四种状态。

  • 若为亮亮,皆大欢喜,满足题目要求
  • 暗亮、亮暗,达到只剩一个为暗的状态;
  • 若为暗暗。则按下编号 100 的灯泡,使编号 99 、100 变为亮,编号 1 的灯泡变为暗,从而达到只剩一个为暗的状态。

步骤二:将灯泡变为全暗

由于灯泡环形摆放,我们指定暗的灯泡编号为 1 ,将剩下 99 个亮着的灯泡每 3 个为一组。按下每组中间的灯泡后,使得所有灯泡变为暗。

步骤三:将灯泡变为全亮

将所有灯泡按一下,灯泡变为全亮。

圆环回原点问题

圆环上有10个点,编号为0~9。从0点出发,每次可以逆时针和顺时针走一步,问走n步回到0点共有多少种走法。

输入: 2
输出: 2
解释:有2种方案。分别是0->1->0和0->9->0

https://codetop.cc/discuss/14

扔鸡蛋问题

https://codetop.cc/discuss/32

可以将楼层划分成多个区间,第一个玻璃球A1用来确定破碎极限属于哪个区间,第二个玻璃球A2按顺序遍历该区间找到破碎极限。那么问题就转换为怎么划分区间满足最坏情况下扔玻璃球次数最少。

A1需要从第一个区间开始遍历到最后一个区间。如果按等大小的方式划分区间,那么A2的遍历次数固定。那么最坏的情况是破碎极限出现在最后一个区间,此时A1遍历的次数最多。为了使最坏情况下A1A2总共遍历的次数比较少,那么后面的区间大小要比前面的区间宽度更小。具体来说,A1每多遍历一次,A2要少遍历一次,才使得破碎极限无论在哪个区间,总共遍历的次数一样。

设第一个区间大小为X,那么第二个区间的大小为X-1,以此类推。那么X + (X-1) + (X-2) + … + 1 = 100,得到X (X + 1) / 2 = 100,即X = 14

切两刀构成三角形的概率

将一条绳子切两刀,求可以构成三角形的概率

答案:四分之一

首先,设绳子总长为L,分为三段的长度分别为x,y,L-x-y。

显然x > 0,y > 0, L-x-y > 0,将这三个约束条件反应在坐标系,如蓝色区域所示。

该区域表示绳子任意切两刀的可行域。

构成三角形的条件是:任意两边之和大于第三边。

因此以下不等式需成立:

① x + y > L – x – y 等价于 y > -x + L/2

② x + (L – x - y) > y 等价于 y < L/2

③ y + (L – x - y) > x 等价于 x < L/2

不等式在坐标系中如绿色区域所示,表示满足三角形条件的可行域,占蓝色区域的1/4。

因此绳子任意切两刀,能构成三角形的概率为1/4

操作系统

交换机有哪些消息转发模式

交换机有三种转发模式:

1)直通式转发

是指交换机在收到数据帧后,不进行缓存和校验,而是直接转发到目的端口。

2)存储式转发

交换机首先在缓冲区存储接收到的整个数据帧,然后进行CRC校验,检查数据帧是否正确,如果正确,再进行转发。如果不正确,则丢弃。

3)碎片隔离式转发

交换机在接收数据帧时,会先缓存数据帧的前64个字节,确保数据帧大于64个字节,再进行转发。

进程与线程 为什么要有进程和线程? 区别是什么?

进程:

  • 程序执行时的一个实例
  • 每个进程都有独立的内存地址空间
  • 系统进行资源分配和调度的基本单位
  • 进程里的堆,是一个进程中最大的一块内存,被进程中的所有线程共享的,进程创建时分配,主要存放 new 创建的对象实例
  • 进程里的方法区,是用来存放进程中的代码片段的,是线程共享的
  • 在多线程 OS 中,进程不是一个可执行的实体,即一个进程至少创建一个线程去执行代码

为什么要有线程?

每个进程都有自己的地址空间,即进程空间。一个服务器通常需要接收大量并发请求,为每一个请求都创建一个进程系统开销大、请求响应效率低,因此操作系统引进线程。

线程:

  • 进程中的一个实体
  • 进程的一个执行路径
  • CPU 调度和分派的基本单位
  • 线程本身是不会独立存在
  • 当前线程 CPU 时间片用完后,会让出 CPU 等下次轮到自己时候在执行
  • 系统不会为线程分配内存,线程组之间只能共享所属进程的资源
  • 线程只拥有在运行中必不可少的资源(如程序计数器、栈)
  • 线程里的程序计数器就是为了记录该线程让出 CPU 时候的执行地址,待再次分配到时间片时候就可以从自己私有的计数器指定地址继续执行
  • 每个线程有自己的栈资源,用于存储该线程的局部变量和调用栈帧,其它线程无权访问

关系:

  • 一个程序至少一个进程,一个进程至少一个线程,进程中的多个线程是共享进程的资源
  • Java 中当我们启动 main 函数时候就启动了一个 JVM 的进程,而 main 函数所在线程就是这个进程中的一个线程,也叫做主线程
  • 一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器,栈区域

如下图

区别:

  • 本质:进程是操作系统资源分配的基本单位;线程是任务调度和执行的基本单位
  • 内存分配:系统在运行的时候会为每个进程分配不同的内存空间,建立数据表来维护代码段、堆栈段和数据段;除了 CPU 外,系统不会为线程分配内存,线程所使用的资源来自其所属进程的资源
  • 资源拥有:进程之间的资源是独立的,无法共享;同一进程的所有线程共享本进程的资源,如内存,CPU,IO 等
  • 开销:每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行程序计数器和栈,线程之间切换的开销小
  • 通信:进程间 以IPC(管道,信号量,共享内存,消息队列,文件,套接字等)方式通信 ;同一个进程下,线程间可以共享全局变量、静态变量等数据进行通信,做到同步和互斥,以保证数据的一致性
  • 调度和切换:线程上下文切换比进程上下文切换快,代
  • 执行过程:每个进程都有一个程序执行的入口,顺序执行序列;线程不能够独立执行,必须依存在应用程序中,由程序的多线程控制机制控制
  • 健壮性:每个进程之间的资源是独立的,当一个进程崩溃时,不会影响其他进程;同一进程的线程共享此线程的资源,当一个线程发生崩溃时,此进程也会发生崩溃,稳定性差,容易出现共享与资源竞争产生的各种问题,如死锁等
  • 可维护性:线程的可维护性,代码也较难调试,bug 难排查

进程与线程的选择:

  • 需要频繁创建销毁的优先使用线程。因为进程创建、销毁一个进程代价很大,需要不停的分配资源;线程频繁的调用只改变 CPU 的执行
  • 线程的切换速度快,需要大量计算,切换频繁时,用线程
  • 耗时的操作使用线程可提高应用程序的响应
  • 线程对 CPU 的使用效率更优,多机器分布的用进程,多核分布用线程
  • 需要跨机器移植,优先考虑用进程
  • 需要更稳定、安全时,优先考虑用进程
  • 需要速度时,优先考虑用线程
  • 并行性要求很高时,优先考虑用线程

Java 编程语言中线程是通过 java.lang.Thread 类实现的。

Thread 类中包含 tid(线程id)、name(线程名称)、group(线程组)、daemon(是否守护线程)、priority(优先级) 等重要属性。

了解死锁吗? 如何避免如何预防

死锁的定义

多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待)

死锁产生的原因

  1. 系统资源的竞争通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。

  2. 进程推进顺序非法进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。

3)信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。

死锁产生的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 循环等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

如何避免死锁

银行家算法

首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。

原理是破环产生的必要条件:

  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
  • 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
  • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏循环等待条件)

在有些情况下死锁是可以避免的。三种用于避免死锁的技术:

  1. 加锁顺序(线程按照一定的顺序加锁)
  2. 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  3. 死锁检测

加锁顺序

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。

如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。

如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(注:获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。

按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(注:并对这些锁做适当的排序),但总有些时候是无法预知的。

加锁时限

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。

死锁检测

  1. 首先为每个进程和每个资源指定一个唯一的号码;
  2. 然后建立资源分配表和进程等待表。

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

死锁的解决措施

1)一种是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(原因同超时类似,不能从根本上减轻竞争)。

2)一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

线程间怎么通信的?

锁机制:包括互斥锁、条件变量、读写锁 、信号量

互斥锁

提供了以排他方式防止数据结构被并发修改的方法。

读写锁

允许多个线程同时读共享数据,而对写操作是互斥的。

条件变量

可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

信号量机制(Semaphore)

包括无名线程信号量和命名线程信号量 。允许多个线程同时进入临界区

信号机制(Signal)

类似进程间的信号处理

线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

线程之间所谓的通信其实主要是为了保证同步,而进程之间的通信主要是为了数据的交换

进程间通信的方式

进程间的通信,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统。以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制。这样进程间的通信就不局限于单台计算机了,实现了网络通信。

进程通信机制主要有:管道、有名管道、信号、信号量、消息队列、共享内存、套接字。

管道( pipe )

管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

有名管道 (named pipe)

有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。(用路径名指出该管道)

信号 (sinal )

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知

信号量(semophore )

信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

消息队列( message queue )

消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

共享内存(shared memory )

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。

套接字(socket )

套解口也是一种进程间通信机制,与其他通信机制不同的是,它实现了实现了网络通信。

消息队列和共享内存的区别?使用场景?

虚拟内存

它是计算机系统中一种内存管理技术,因为计算机内存的造价比较高,一般常见内存8G,而计算机的应用软件都是在内存中运行,每个应用都会占一定的内存,这就导致一个问题,如果计算机同时运行很多软件,使计算机的内存空间很容易被占满,那么计算机就无法运行用户的下一个应用了,直到空出足够的内存。虚拟内存技术就是解决用户内存不足的问题,当执行程序占用了大部分内存,导致当前内存不足以支撑下一个应用时,系统便会”拿”出一部分物理内存来充当内存使用。具体操作流程是这样的,程序运行的时候,可以先将一部分需要运行的程序装入内存,剩余部分暂且放在外存,当这部分运行结束需要运行下一部分时,将已运行的部分从内存转移到外存,同时将外存需要的那部分转移到内存,这样就可以避免因为内存不足引起的程序无法运行的问题。

虚拟内存是一些系统页文件,存放在磁盘上,每个系统页文件大小为4K,物理内存也被分页,每个页大小也为4K,这样虚拟页文件和物理内存页就可以对应,实际上虚拟内存就是用于物理内存的临时存放的磁盘空间。页文件就是内存页,物理内存中每页叫物理页,磁盘上的页文件叫虚拟页,物理页+虚拟页就是系统所有使用的页文件的总和。

Kill工作原理

向Linux系统的内核发送一个系统操作信号和某个程序的进程标识号,然后系统内核就可以对进程标识号进行操作。

Linux的硬链接与软链接

硬链接:假设B是A的硬链接,那么他们两个指向了同一个文件!允许一个文件拥有多个路径,用户可以通过这种机制来建立硬链接到一些重要文件上,防止误删!

软链接:类似Windows下的快捷方式,删除了源文件,快捷方式也访问不了

SQL

分库分表怎么设计

https://blog.csdn.net/weixin_33570610/article/details/112459378

分表

场景:对于大型的互联网应用来说,数据库单表的记录行数可能达到千万级甚至是亿级,并且数据库面临着极高的并发访问。采用Master-Slave复制模式的MySQL架构,只能够对数据库的读进行扩展,而对数据库的写入操作还是集中在Master上,并且单个Master挂载的Slave也不可能无限制多,Slave的数量受到Master能力和负载的限制。

对于访问极为频繁且数据量巨大的单表(百万到千万级别)来说,我们首先要做的就是减少单表的记录条数,以便减少数据查询所需要的时间,提高数据库的吞吐,这就是所谓的分表!

在分表之前,首先需要选择适当的分表策略,使得数据能够较为均衡地分不到多张表中,并且不影响正常的查询!

对于互联网企业来说,大部分数据都是与用户关联的,因此,用户id是最常用的分表字段。因为大部分查询都需要带上用户id,这样既不影响查询,又能够使数据较为均衡地分布到各个表中(当然,有的场景也可能会出现冷热数据分布不均衡的情况),如下图:

假设有一张表记录用户购买信息的订单表order,由于order表记录条数太多,将被拆分成256张表

拆分的记录根据user_id%256取得对应的表进行存储,前台应用则根据对应的user_id%256,找到对应订单存储的表进行访问(即id除以256余数为0则查0号表)

这样一来,user_id便成为一个必需的查询条件,否则将会由于无法定位数据存储的表而无法对数据进行访问。

注:拆分后表的数量一般为2的n次方,就是上面拆分成256张表的由来!

原因是2的N次方的话,计算机内部就可以使用位运算实现了。主要是运算速度提高了。

分库

场景:分表能够解决单表数据量过大带来的查询效率下降的问题,但是,却无法给数据库的并发处理能力带来质的提升。面对高并发的读写访问,当数据库master服务器无法承载写操作压力时,不管如何扩展slave服务器,此时都没有意义了。

因此,我们必须换一种思路,对数据库进行拆分,从而提高数据库写入能力,这就是所谓的分库!

与分表策略相似,分库可以采用通过一个关键字取模的方式,来对数据访问进行路由,如下图所示:

假设user_id 字段的值为258,将原有的单库分为256个库,那么应用程序对数据库的访问请求将被路由到第二个库(258%256 = 2)。

分库分表

场景:有时数据库可能既面临着高并发访问的压力,又需要面对海量数据的存储问题,这时需要对数据库既采用分表策略,又采用分库策略,以便同时扩展系统的并发处理能力,以及提升单表的查询性能,这就是所谓的分库分表。

分库分表的策略比前面的仅分库或者仅分表的策略要更为复杂,一种分库分表的路由策略如下:

  1. 中间变量 = user_id % (分库数量 * 每个库的表数量)
  2. 库 = 取整数 (中间变量 / 每个库的表数量)
  3. 表 = 中间变量 % 每个库的表数量

同样采用user_id作为路由字段,首先使用user_id 对库数量*每个库表的数量取模,得到一个中间变量;然后使用中间变量除以每个库表的数量,取整,便得到对应的库;而中间变量对每个库表的数量取模,即得到对应的表。

分库分表策略详细过程如下:

假设将原来的单库单表order拆分成256个库,每个库包含1024个表,那么按照前面所提到的路由策略,对于user_id=262145 的访问,路由的计算过程如下:

  1. 中间变量 = 262145 % (256 * 1024) = 1
  2. 库 = 取整 (1/1024) = 0
  3. 表 = 1 % 1024 = 1

这就意味着,对于user_id=262145 的订单记录的查询和修改,将被路由到第0个库的第1个order_1表中执行!!!

分表策略

分表又分为横向分表和纵向分表

纵向分表

将本来可以在同一个表的内容,人为划分为多个表。(所谓的本来,是指按照关系型数据库的第三范式要求,是应该在同一个表的。)

分表理由:根据数据的活跃度进行分离,(因为不同活跃的数据,处理方式是不同的)

案例:

对于一个博客系统,文章标题,作者,分类,创建时间等,是变化频率慢查询次数多,而且最好有很好的实时性 的数据,我们把它叫做冷数据。而博客的浏览量,回复数等,类似的统计信息,或者别的变化频率比较高的数据,我们把它叫做活跃数据。所以,在进行数据库结构设计的时候,就应该考虑分表,首先是纵向分表的处理。

这样纵向分表后:

首先存储引擎的使用不同,冷数据使用MyIsam 可以有更好的查询数据。**活跃数据,可以使用Innodb,**可以有更好的更新速度。

其次,对冷数据进行更多的从库配置,因为更多的操作时查询,这样来加快查询速度。对热数据,可以相对有更多的主库的横向分表处理

其实,对于一些特殊的活跃数据,也可以考虑使用memcache ,redis之类的缓存,等累计到一定量再去更新数据库。或者mongodb 一类的nosql数据库。

垂直切分的优点:

  • 解决业务系统层面的耦合,业务清晰
  • 与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等
  • 高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈

缺点:

  • 部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度
  • 分布式事务处理复杂
  • 依然存在单表数据量过大的问题(需要水平切分)

横向分表(水平分表)

​ 字面意思,就可以看出来,是把大的表结构,横向切割为同样结构的不同表,如,用户信息表,user_1,user_2等。表结构是完全一样,但是,根据某些特定的规则来划分的表,如根据用户ID来取模划分。

分表理由:根据数据量的规模来划分,保证单表的容量不会太大,从而来保证单表的查询等处理能力

案例:

同上面的例子,博客系统。当博客的量达到很大时候,就应该采取横向分割来降低每个单表的压力,来提升性能。例如博客的冷数据表,假如分为100个表,当同时有100万个用户在浏览时,如果是单表的话,会进行100万次请求,而现在分表后,就可能是每个表进行1万个数据的请求(因为,不可能绝对的平均,只是假设),这样压力就降低了很多很多。

水平切分的优点:

  • 不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力
  • 应用端改造较小,不需要拆分业务模块

缺点:

  • 跨分片的事务一致性难以保证
  • 跨库的join关联查询性能较差
  • 数据多次扩展难度和维护量极大

分库分表带来的问题

https://blog.csdn.net/weixin_42190794/article/details/85611566

1、事务一致性问题

分布式事务

当更新内容同时分布在不同库中,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,一般可使用"XA协议"和"两阶段提交"处理。

分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间。导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平扩展的枷锁。

最终一致性

对于那些性能要求很高,但对一致性要求不高的系统,往往不苛求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误后立即回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等等。事务补偿还要结合业务系统来考虑。

2、跨节点关联查询 join 问题

表分布在不同的节点上,考虑到性能,尽量避免使用join查询

解决这个问题的一些方法:

1)全局表

全局表,也可看做是"数据字典表",就是系统中所有模块都可能依赖的一些表,为了避免跨库join查询,可以将这类表在每个数据库中都保存一份。这些数据通常很少会进行修改,所以也不担心一致性的问题。

2)字段冗余

一种典型的反范式设计,利用空间换时间,为了性能而避免join查询。例如:订单表保存userId时候,也将userName冗余保存一份,这样查询订单详情时就不需要再去查询"买家user表"了。

但这种方法适用场景也有限,比较适用于依赖字段比较少的情况。而冗余字段的数据一致性也较难保证,就像上面订单表的例子,买家修改了userName后,是否需要在历史订单中同步更新呢?这也要结合实际业务场景进行考虑。

3)数据组装

在系统层面,分两次查询,第一次查询的结果集中找出关联数据id,然后根据id发起第二次请求得到关联数据。最后将获得到的数据进行字段拼装。

4)ER分片

关系型数据库中,如果可以先确定表之间的关联关系,并将那些存在关联关系的表记录存放在同一个分片上,那么就能较好的避免跨分片join问题。在1:1或1:n的情况下,通常按照主表的ID主键切分。

这样一来,Data Node1上面的order订单表与orderdetail订单详情表就可以通过orderId进行局部的关联查询了,Data Node2上也一样。

3、跨节点分页、排序、函数问题

跨节点多库进行查询时,会出现limit分页、order by排序等问题。分页需要按照指定字段进行排序,当排序字段就是分片字段时,通过分片规则就比较容易定位到指定的分片;当排序字段非分片字段时,就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。

为了排序的准确性,需要将所有节点的前N页数据都排序好做合并,最后再进行整体的排序,这样的操作时很耗费CPU和内存资源的,所以页数越大,系统的性能也会越差。

在使用Max、Min、Sum、Count之类的函数进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回

4、全局主键避重问题

在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成的ID无法保证全局唯一。因此需要单独设计全局主键,以避免跨库主键重复问题。有一些常见的主键生成策略:

1)UUID

UUID标准形式包含32个16进制数字,分为5段,形式为8-4-4-4-12的36个字符,例如:550e8400-e29b-41d4-a716-446655440000,UUID是主键是最简单的方案,本地生成,性能高,没有网络耗时。但缺点也很明显,由于UUID非常长,会占用大量的存储空间;另外,作为主键建立索引和基于索引进行查询时都会存在性能问题,在InnoDB下,UUID的无序性会引起数据位置频繁变动,导致分页。

2)结合数据库维护主键ID表

在数据库中建立 sequence 表:

CREATE TABLE `sequence` (    `id` bigint(20) unsigned NOT NULL auto_increment,    `stub` char(1) NOT NULL default '',    PRIMARY KEY  (`id`),    UNIQUE KEY `stub` (`stub`)  ) ENGINE=MyISAM;

stub字段设置为唯一索引,同一stub值在sequence表中只有一条记录,可以同时为多张表生成全局ID。sequence表的内容,如下所示:

+-------------------+------+  | id                | stub |  +-------------------+------+  | 72157623227190423 |    a |  +-------------------+------+  

使用 MyISAM 存储引擎而不是 InnoDB,以获取更高的性能。MyISAM使用的是表级别的锁,对表的读写是串行的,所以不用担心在并发时两次读取同一个ID值。

当需要全局唯一的64位ID时,执行:

REPLACE INTO sequence (stub) VALUES ('a');  SELECT LAST_INSERT_ID()

使用replace into代替insert into好处是避免了表行数过大,不需要另外定期清理。

此方案较为简单,但缺点也明显:存在单点问题,强依赖DB,当DB异常时,整个系统都不可用。配置主从可以增加可用性,但当主库挂了,主从切换时,数据一致性在特殊情况下难以保证。另外性能瓶颈限制在单台MySQL的读写性能。

3)Snowflake分布式自增ID算法

Twitter的snowflake算法解决了分布式系统生成全局ID的需求,生成64位的Long型数字,组成部分:

  • 第一位未使用
  • 接下来41位是毫秒级时间,41位的长度可以表示69年的时间
  • 5位datacenterId,5位workerId。10位的长度最多支持部署1024个节点
  • 最后12位是毫秒内的计数,12位的计数顺序号支持每个节点每毫秒产生4096个ID序列

这样的好处是:毫秒数在高位,生成的ID整体上按时间趋势递增;不依赖第三方系统,稳定性和效率较高,理论上QPS约为409.6w/s(1000*2^12),并且整个分布式系统内不会产生ID碰撞;可根据自身业务灵活分配bit位。

不足就在于:强依赖机器时钟,如果时钟回拨,则可能导致生成ID重复。

5、数据迁移、扩容问题

当业务高速发展,面临性能和存储的瓶颈时,才会考虑分片设计,此时就不可避免的需要考虑历史数据迁移的问题。一般做法是先读出历史数据,然后按指定的分片规则再将数据写入到各个分片节点中。此外还需要根据当前的数据量和QPS,以及业务发展的速度,进行容量规划,推算出大概需要多少分片(一般建议单个分片上的单表数据量不超过1000W)

如果采用数值范围分片,只需要添加节点就可以进行扩容了,不需要对分片数据迁移。如果采用的是数值取模分片,则考虑后期的扩容问题就相对比较麻烦。

水平分表依据什么分?时间还是数据

当数据量越来越大时,需要对数据库进行水平切分,上文描述的切分方法有"根据数值范围"和"根据数值取模"。

“根据数值范围”:以主键uid为划分依据,按uid的范围将数据水平切分到多个数据库上。例如:user-db1存储uid范围为01000w的数据,user-db2存储uid范围为1000w2000wuid数据。

  • 优点是:扩容简单,如果容量不够,只要增加新db即可。
  • 不足是:请求量不均匀,一般新注册的用户活跃度会比较高,所以新的user-db2会比user-db1负载高,导致服务器利用率不平衡

“根据数值取模”:也是以主键uid为划分依据,按uid取模的值将数据水平切分到多个数据库上。例如:user-db1存储uid取模得1的数据,user-db2存储uid取模得0的uid数据。

  • 优点是:数据量和请求量分布均均匀
  • 不足是:扩容麻烦,当容量不够时,新增加db,需要rehash。需要考虑对数据进行平滑的迁移

分库分表之后我想查询近期的订单,怎么查

https://www.cnblogs.com/plumswine/p/14196962.html

1)可以通过Elasticsearch来构建二级索引

可以将所需要的进行判断的字段, 简单理解也就是SQL语句中WHERE语句中进行判断的字段, 以及分库分表的分区字段, 在这道题中就是订单ID, 存储进Elasticsearch中. 构建一个二级索引, 每次查询的时候现在Elasticsearch中进行查询, 获取到订单ID, 然后再根据订单ID去MySQL执行查询就好了, 如果是聚合查询, 那么就需要自行在server中进行聚合

整个过程类似于MySQL中的非主键索引在执行非覆盖索引查询的时候, 执行的回表操作

2)直接把全量数据存储在ES中的方式来处理分库分表之后的多条件查询以及JOIN查询

其实还可以将数据表的全部字段以及数据再存储一份到Elasticsearch, 查询的时候直接查询Elasticsearch中的数据就好了, 也不需要再查询数据库, 缺点是Elasticsearch中存储的数据会很多

数据存储引擎有哪些

InnoDB、MyISAM、Memory、Merge

InnoDB和MylSAM的区别

1、事务支持:MyISAM不支持事务处理,InnoDB支持事务处理。

2、锁级别:MyISAM只支持表级锁,InnoDB支持行级锁和表级锁,默认使用行级锁,但是InnoDB的行锁是通过给索引项加锁来实现的,即只有通过索引进行查询数据,InnoDB才使用行级锁,否则将使用表锁。行级锁在每次获取锁和释放锁的操作需要消耗比表锁更多的资源。使用行锁可能会存在死锁的情况,但是表级锁不存在死锁。

3、表主键与外键约束:

(1)MyISAM:允许没有任何索引和主键的表存在。不支持外键。

(2)InnoDB:支持主键自增长列且主键不能为空,如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见)。支持外键完整性约束。

4、索引结构:MyISAM和InnoDB都是使用B+树索引,MyISAM的主键索引和辅助索引的Data域都是保存行的地址,但是InnoDB的主键索引保存的不是行的地址,而是保存该行的所有所有数据,而辅助索引的Data域保存的则是主索引的值。

5、全文索引:MyISAM支持FULLTEXT类型的全文索引,InnoDB不支持全文索引(5.6版本之后InnoDB存储引擎开始支持全文索引)

6、存储结构:

(1)MyISAM会在磁盘上存储成三个文件。

.frm:存储表定义
.MYD:存储数据
.MYI:存储索引
(2)InnoDB:把数据和索引存放在表空间里面,所有的表都保存在同一个数据文件中,InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。

7、存储空间:

(1)MyISAM:可被压缩,存储空间较小。支持三种不同的存储格式:静态表(默认,但是注意数据末尾不能有空格,会被去掉)、动态表、压缩表。

(2)InnoDB:需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。

8、表的具体行数:

(1)MyISAM:保存有表的总行数,如果select count() from table;会直接取出出该值,不需要进行全表扫描。

(2)InnoDB:没有保存表的总行数,如果使用select count() from table;需要会遍历整个表,消耗相当大。

9、适用场景:

(1)如果需要提供回滚、崩溃恢复能力的ACID事务能力,并要求实现行锁级别并发控制,InnoDB是一个好的选择;

(2)如果数据表主要用来查询记录,读操作远远多于写操作且不需要数据库事务的支持,则MyISAM引擎能提供较高的处理效率;

select count(*)时InnoDB和MylSAM分别是怎么处理的

InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,性能是毫秒级。

聚簇索引和非聚簇索引的区别

  • 聚簇索引:将数据存储和索引放在一起、并且是按照一定的顺序组织的,找到索引也就找到了数据,数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻的存放在磁盘上的。

  • 非聚簇索引:叶子节点不存储数据,存储的是数据行地址,也就是说根据索引查找到数据行的位置再去磁盘查找数据,这就有点类似一本书的目录,比如要找到第三章第一节,那就现在目录里面查找,找到对应的页码后再去对应的页码看文章

    InnoDB中一定有主键,主键一定是聚簇索引,不手动设置,则会使用一个unique索引作为主键索引,没有unique索引,则会使用数据库内部的一个隐藏行id来当作主键索引。在聚簇索引之上创建的索引称为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引,前缀索引、唯一索引。辅助索引叶子节点存储的不再是行的物理位置,而是主键值。

    MyISM使用的是非聚簇索引,没有聚簇索引。

假如数据库某个字段是String类型,读的时候用int类型去接收会有什么问题?反过来呢?

事务有哪些隔离级别,分别解决了什么问题

MySQL的事务隔离级别有四种:

1.读未提交(read-uncommitted):能读到未提交的数据。会出现脏读、不可重复读、幻读。

2.读已提交(read-committed):读已提交的数据。会出现不可重复读和幻读。

3.可重复读(repeatable-read):mysql默认的事务隔离级别,查询的都是事务开始时的数据。只会出现幻读。

4.串行读(serializable):完全串行化读,每次都会锁表,读写互相阻塞。最高隔离级别,不会出现脏读,不可重复读,幻读。但会大大影响系统的性能,一般不用。

可重复读是怎么解决脏读的

解决方案:

1) 脏读:修改时加排他锁,直到事务提交后才释放,读取时加共享锁,读取完释放事务1读取数据时加上共享锁后(这 样在事务1读取数据的过程中,其他事务就不会修改该数据),不允许任何事物操作该数据,只能读取,之后1如果有更新操作,那么会转换为排他锁,其他事务更 无权参与进来读写,这样就防止了脏读问题。

​ 但是当事务1读取数据过程中,有可能其他事务也读取了该数据,读取完毕后共享锁释放,此时事务1修改数据,修改 完毕提交事务,其他事务再次读取数据时候发现数据不一致,就会出现不可重复读问题,所以这样不能够避免不可重复读问题。

2)不可重复读:读取数据时加共享锁,写数据时加排他锁,都是事务提交才释放锁。读取时候不允许其他事物修改该数据,不管数据在事务过程中读取多少次,数据都是一致的,避免了不可重复读问题

3)幻读问题:采用的是范围锁RangeS RangeS_S模式,锁定检索范围为只读,这样就避免了幻影读问题

有俩事务,事务A:读、写(张三)、读 事务B:读、写(李四)、读,假如俩事务同时执行,结果如何

分情况讨论分析,不一定谁执行快

索引的结构? b+树为什么比b树矮胖?

SQL语句题 学生表 搜索各班级分数最高的学生

为什么innodb要选择B+树

索引在 MySQL 数据库中分三类:

  • B+树索引
  • Hash索引
  • 全文索引

使用场景决定设计,B+树经常用作数据库的索引,数据库中的select操作并不是只返回一条数据而是多条。如果用B树的话,可能需要跨层访问,而B+树由于所有数据都在叶子结点,不用跨层,同时链表有序只要找到首尾,就可以定位到所有符合条件的数据。这就是B+树比B树更优的地方。

Hash更快,为什么数据库还用B+树作索引?与业务场景有关,如果只选一个数据,hash确实更快,但是select经常要选择多条,这时由于B+树索引有序并且又有链表相连,它的查询效率比hash更快。而且数据库的索引是存储在磁盘上的,数据量大的情况下无法一次性装入内存,B+树的多路设计可以允许索引数据分批加载到内存,树的高度也很低、

乐观锁和悲观锁

没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。

实现并发控制的主要手段分为乐观并发控制和悲观并发控制两种

悲观锁(Pessimistic Lock)

具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:

  1. 传统的关系型数据库使用这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
  2. Java 里面的同步 synchronized 关键字的实现。

悲观锁主要分为共享锁和排他锁:

  • 共享锁【shared locks】又称为读锁,简称 S 锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 排他锁【exclusive locks】又称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改。

悲观锁实现方式

  1. 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locks)。

  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。

  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。

  4. 期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。

使用 select…for update 锁数据,需要注意锁的级别,MySQL InnoDB 默认行级锁。行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。

乐观锁(Optimistic Locking)

乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表。

乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量

乐观锁采取了更加宽松的加锁机制。也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:

  1. CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
  2. 版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

乐观锁实现方式乐观锁不需要借助数据库的锁机制

主要就是两个步骤:冲突检测和数据更新。比较典型的就是 CAS (Compare and Swap)。

CAS 即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS 操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值(V)与预期原值(A)相匹配,那么处理器会自动将该位置值更新为新值(B)。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置(V)应该包含值(A)。如果包含该值,则将新值(B)放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。Java 中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个 CAS。java.util.concurrent包下大量的类都使用了这个 Unsafe.java 类的 CAS 操作。

当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。比如前面的扣减库存问题,通过乐观锁可以实现如下:

# quantity = 3
select quantity from items where id=1
# 修改其为2
update items set quantity=2 where id=1 and quantity=3

在更新之前,先查询一下库存表中当前库存数(quantity),然后在做 update 的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。

CAS带来的问题

ABA问题

  1. 比如说线程一从数据库中取出库存数 3,这时候线程二也从数据库中取出库存数 3,并且线程二进行了一些操作变成了 2。

  2. 然后线程二又将库存数变成 3,这时候线程一进行 CAS 操作发现数据库中仍然是 3,然后线程一操作成功。

  3. 尽管线程一的 CAS 操作成功,但是不代表这个过程就是没有问题的。

解决办法:

就是通过一个单独的可以顺序递增的 version 字段。除了 version 以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。

自旋循环时间消耗

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

解决办法:

1.代码层面,破坏掉for死循环,当超过一定时间或者一定次数时,return退出。

  1. 使用类似ConcurrentHashMap的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来,能降低CPU消耗,但是治标不治本。

3.使用JVM能支持处理器提供的pause指令来提升效率。pause指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空,从而提高CPU的实行效率。

CAS只能单变量

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ji=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之前的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

CAS底层

假如说有 3 个线程并发的要修改一个 AtomicInteger 的值,底层机制如下:

  1. 首先,每个线程都会先获取当前的值,接着走一个原子的 CAS 操作。原子的意思就是这个 CAS 操作一定是自己完整执行完的,不会被别人打断。
  2. 然后 CAS 操作里,会比较一下,现在的值是不是刚才获取到的那个值。如果是,说明没人改过这个值,然后设置成累加 1 之后的一个值。
  3. 同理,如果有人在执行 CAS 的时候,发现之前获取的值跟当前的值不一样,会导致 CAS 失败。失败之后,进入一个无限循环,再次获取值,接着执行 CAS 操作。

CAS 性能优化

从流程图可以看出来,大量的线程同时并发修改一个 AtomicInteger,可能有很多线程会不停的自旋,进入一个无限重复的循环中。这些线程不停地获取值,然后发起 CAS 操作,但是发现这个值被别人改过了,于是再次进入下一个循环,获取值,发起 CAS 操作又失败了,再次进入下一个循环。在大量线程高并发更新 AtomicInteger 的时候,这种问题可能会比较明显,导致大量线程空循环,自旋转,性能和效率都不是特别好。那么如何优化呢?

Java8 有一个新的类,LongAdder,它就是尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能,这个类具体是如何优化性能的呢?如图:

LongAdder 核心思想就是热点分离,这一点和 ConcurrentHashMap 的设计思想相似。就是将 value 值分离成一个数组,当多线程访问时,通过 hash 算法映射到其中的一个数字进行计数。而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度。

如何选择

1)响应效率:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
2)冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率。冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大。
3)重试代价:如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
4)乐观锁如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户从新操作。悲观锁则会等待前一个更新完成。这也是区别。

MVCC

基本原理

MVCC的实现,通过保存数据在某个时间点的快照来实现的。这意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。根据事务开始的时间不同,同时也意味着在同一个时刻不同事务看到的相同表里的数据可能是不同的。

基本特征

  • 每行数据都存在一个版本,每次数据更新时都更新该版本。
  • 修改时Copy出当前版本随意修改,各个事务之间无干扰。
  • 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)

InnoDB存储引擎MVCC的实现策略

在每一行数据中额外保存两个隐藏的列:当前行创建时的版本号和删除时的版本号(可能为空,其实还有一列称为回滚指针,用于事务回滚,不在本文范畴)。这里的版本号并不是实际的时间值,而是系统版本号。每开始新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询每行记录的版本号进行比较。

每个事务又有自己的版本号,这样事务内执行CRUD操作时,就通过版本号的比较来达到数据版本控制的目的。

  • trx_id这个id用来存储的每次对某条聚簇索引记录进行修改的时候的事务id。
  • roll_pointer每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。

MVCC下InnoDB的增删查改是怎么work的

1. 插入

插入数据(insert):记录的版本号即当前事务的版本号

执行一条数据语句:insert into testmvcc values(1,“test”);

假设事务id为1,那么插入后的数据行如下:

2、更新

在更新操作的时候,采用的是先标记旧的那行记录为已删除,并且删除版本号是事务版本号,然后插入一行新的记录的方式。

比如,针对上面那行记录,事务Id为2 要把name字段更新

update table set name= ‘new_value’ where id=1;

3、删除

删除操作的时候,就把事务版本号作为删除版本号。比如

delete from table where id=1;

4、查询

查询操作:

从上面的描述可以看到,在查询时要符合以下两个条件的记录才能被事务查询出来:

  1. 删除版本号未指定或者大于当前事务版本号,即查询事务开启后确保读取的行未被删除。(即上述事务id为2的事务查询时,依然能读取到事务id为3所删除的数据行)

  2. 创建版本号 小于或者等于 当前事务版本号 ,就是说记录创建是在当前事务中(等于的情况)或者在当前事务启动之前的其他事物进行的insert。

(即事务id为2的事务只能读取到create version<=2的已提交的事务的数据集)

补充:

1.MVCC手段只适用于Msyql隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read).

2.Read uncimmitted由于存在脏读,即能读到未提交事务的数据行,所以不适用MVCC.

原因是MVCC的创建版本和删除版本只要在事务提交后才会产生。

3.串行化由于是会对所涉及到的表加锁,并非行锁,自然也就不存在行的版本控制问题。

4.通过以上总结,可知,MVCC主要作用于事务性的,有行锁控制的数据库模型

读操作的分类

在MVCC并发控制中,读操作可以分成两类:

  1. 快照读 (snapshot read):读取的是记录的可见版本 (有可能是历史版本),不用加锁(共享读锁s锁也不加,所以不会阻塞其他事务的写)
  2. 当前读 (current read):读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录

innodb 有哪几种锁?

共享/排它锁(Shared and Exclusive Locks)

共享锁(Share Locks,记为S锁),读取数据时加S锁

排他锁(eXclusive Locks,记为X锁),修改数据时加X锁

意向锁(Intention Locks)

InnoDB为了支持多粒度锁机制(multiple granularity locking),即允许行级锁与表级锁共存,而引入了意向锁(intention

locks)。意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。

意向锁是一个表级别的锁(table-level locking);

意向锁又分为:

意向共享锁(intention shared lock, IS),它预示着,事务有意向对表中的某些行加共享S锁;

意向排它锁(intention exclusive lock, IX),它预示着,事务有意向对表中的某些行加排它X锁;

加锁的语法为:

select … lock in share mode;  要设置IS锁;

select … for update;       要设置IX锁;

事务要获得某些行的S/X锁,必须先获得表对应的IS/IX锁,意向锁仅仅表明意向,意向锁之间相互兼容,兼容互斥表如下:

IS IX

IS 兼 容 兼 容

IX 兼 容 兼 容

虽然意向锁之间互相兼容,但是它与共享锁/排它锁互斥,其兼容互斥表如下:

S X

IS 兼 容 互 斥

IX 互 斥 互 斥

记录锁(Record Locks)

记录锁,它封锁索引记录,例如(其中id为pk):

create table lock_example(id smallint(10),name varchar(20),primary key id)engine=innodb;

数据库隔离级别为RR,表中有如下数据:

10, zhangsan

20, lisi

30, wangwu

select * from t where id=1 for update;

其实这里是先获取该表的意向排他锁(IX),再获取这行记录的排他锁(我的理解是因为这里直接命中索引了),以阻止其他事务插入,更新,删除id=1的这一行。

间隙锁(Gap Locks)

它封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。依然是上面的例子,InnoDB,RR:

select * from lock_example

where id between 8 and 15

for update;

这个SQL语句会封锁区间(8,15),以阻止其他事务插入id位于该区间的记录。

间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。如果把事务的隔离级别降级为读提交(Read Committed, RC),间隙锁则会自动失效。

临键锁(Next-key Locks)

临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。

默认情况下,innodb使用next-key locks来锁定记录。但当查询的索引含有唯一属性的时候,Next-Key Lock

会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。

插入意向锁(Insert Intention Locks)

对已有数据行的修改与删除,必须加强互斥锁(X锁),那么对于数据的插入,是否还需要加这么强的锁,来实施互斥呢?插入意向锁,孕育而生。插入意向锁,是间隙锁(GapLocks)的一种(所以,也是实施在索引上的),它是专门针对insert操作的。多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此

自增锁(Auto-inc Locks)

自增锁是一种特殊的表级别锁(table-level

lock),专门针对事务插入AUTO_INCREMENT类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。

什么时候表锁?什么时候行锁?

只有通过索引条件检索数据,InnoDB才会使用行级锁,否则,InnoDB将使用表锁
在MySQL中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。 在UPDATE、DELETE操作时,MySQL不仅锁定WHERE条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的next-key locking

为什么有 gap lock?

  1. 简单讲就是防止幻读。通过锁阻止特定条件的新记录的插入,因为插入时也要获取gap锁(Insert Intention Locks)。

  2. 它结合了索引行锁定和间隙锁定。InnoDB执行行级锁的方式是这样的:当它搜索或扫描表索引时,它会在遇到的索引记录上设置共享锁或排他锁。

Mysql执行解析过程

  • 客户端发送一条查询给服务器;
  • 服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。
  • 服务器端进行SQL解析、预处理,在优化器生成对应的执行计划;
  • mysql根据优化器生成的执行计划,调用存储引擎的API来执行查询。
  • 将结果返回给客户端。

连接

客户端想要读写数据,第一步得跟服务端建立连接

查询缓存

MySQL的缓存默认是关闭的,因为sql语句不一样或者表里面数据某一条发生了变化,缓存不在起作用,浪费时间

语法解析和预处理

1)词法分析就是把一个完整的SQL语句打碎成一个个的单词;

2)第二步就是语法分析,语法分析会对SQL做一些语法检查,比如单引号有没有闭合,然后根据MySQL定义的语法规则,根据SQL语句生成一个数据结构。这个数据结构我们把它叫做解析树(select_lex);

3)如果写了一个词法和语法都正确的SQL,但是表名或者字段不存在,会在哪里报错?
是解析的时候报错还是执行的时候报错?
实际上还是在解析的时候报错,解析SQL的环节里面有个预处理器。
它会检查生成的解析树,解决解析器无法解析的语义。比如,它会检查表和列名是否存在,检查名字和别名,保证没有歧义。
预处理之后得到一个新的解析树。

查询优化(Query Optimizer)与查询执行计划

解析树是一个可以被执行器认识的数据结构。

一条SQL语句是可以有很多种执行方式的(遍历树的方式),最终返回相同的结果,他们是等价的。但是如果有这么多种执行方式,这些执行方式怎么得到的?最终选择哪一种去执行?根据什么判断标准去选择?
这个就是MySQL的查询优化器的模块(Optimizer)。
查询优化器的目的就是根据解析树生成不同的执行计划(ExecutionPlan),然后选择一种最优的执行计划,MySQL里面使用的是基于开销(cost)的优化器,那种执行计划开销最小,就用哪种。

MySQL的优化器能处理哪些优化类型呢?

举两个简单的例子:
1、当我们对多张表进行关联查询的时候,以哪个表的数据作为基准表(先访问哪张表)。
2、有多个索引可以使用的时候,选择哪个索引。
3、对于查询条件的优化,比如移除1=1 之类的恒等式,移除不必要的括号,表达式的计算,子查 询和连接查询的优化。

······

优化器最终会把解析树变成一个执行计划(execution_plans),执行计划也是一个数据结构。这个执行计划不一定是最优的,可以通过explain 语句来查看执行计划到底使用了哪些索引,可以使用forge index,ignore index来手动控制一下

explain select count(*) from vipshop_finance_account IGNORE INDEX( idx_open_wpb_status);

explain select * from vipshop_trade_log FORCE INDEX (idx_related_tradeid) where related_tradeId>'004201509151046563846447';

总结:一条SQL语句的执行流程大致为,客户端先与MySQL服务器建立连接,然后,发送一条查询语句,如果MySQL开启了缓存的话,会将SQL语句存到缓存中,然后,解析器进行词法和语法的解析,接着预处理器会进行再次检查,比如检查表名和列名是否存在,没有问题的话,优化器会对SQL语句进行优化,生成一个执行计划,交给执行器执行SQL,执行器调用存储引擎,存储引擎读取磁盘数据,将查询结果交给执行器,执行器再将查询结果反馈给客户端或缓存。

存储引擎

innodb,memory,myisam

执行引擎(Execution Engine)

执行器,或者叫执行引擎,它利用存储引擎提供的相应的API来完成操作。最后把数据返回给客户端,即使没有结果也要返回。

Mysql语句执行顺序

  1. from
  2. join
  3. on
  4. where
  5. group by(开始使用select中的别名,后面的语句中都可以使用)
  6. avg,sum…
  7. having
  8. select
  9. distinct
  10. order by
  11. limit

视图

基本表是本身独立存在的表,在 SQL 中一个关系就对应一个表。 视图是从一个或几个基本表导出的表。视图本身不独立存储在数据库中,是一个虚表,它的优点是:

(1) 视图能够简化用户的操作

(2) 视图使用户能以多种角度看待同一数据;

(3) 视图为数据库提供了一定程度的逻辑独立性;

(4) 视图能够对机密数据提供安全保护。

日志

MySQL中有六种日志文件,分别是:

  1. 重做日志(redo log)
  2. 回滚日志(undo log)
  3. 二进制日志(binlog)
  4. 错误日志(errorlog)
  5. 慢查询日志(slow query log)
  6. 一般查询日志(general log)
  7. 中继日志(relay log)

其中重做日志和回滚日志与事务操作息息相关,二进制日志也与事务操作有一定的关系。

事务是如何通过日志来实现的?

Undo 记录某 数据 被修改 前 的值,可以用来在事务失败时进行 rollback;
Redo 记录某 数据块 被修改 后 的值,可以用来恢复未写入 data file 的已成功事务更新的数据。
即,

Redo Log 保证事务的持久性
Undo Log 保证事务的原子性(在 InnoDB 引擎中,还用 Undo Log 来实现 MVCC)
  比如某一时刻数据库 DOWN 机了,有两个事务,一个事务已经提交,另一个事务正在处理。数据库重启的时候就要根据日志进行前滚及回滚,把已提交事务的更改写到数据文件,未提交事务的更改恢复到事务开始前的状态。即通过 redo log 将所有已经在存储引擎内部提交的事务应用 redo log 恢复,所有已经 prepared 但是没有 commit 的事务将会应用 undo log 做回滚。重做日志(redo log):

redo log

redo log在事务没有提交前,会记录每一个修改操作变更后的数据。主要是防止在发生故障的时间点,尚有脏页未写入磁盘。在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。(作用)

在事务提交前,只要将 Redo Log 持久化即可,不需要将数据持久化。当系统崩溃时,系统可以根据redo Log的内容,将所有数据恢复到最新的状态。(持久化:先将重做日志写入缓存,再刷新(fsync)到磁盘)

重做日志是物理日志,记录的是对于每个页的修改。事务开始后Innodb存储引擎先将重做日志写入缓存(innodb_log_buffer)中。然后会通过以下三种方式将innodb日志缓冲区的日志刷新到磁盘。

Master Thread每秒一次执行刷新Innodb_log_buffer到重做日志文件。
每个事务提交时会将重做日志刷新到重做日志文件。
当重做日志缓存可用空间少于一半时,重做日志缓存被刷新到重做日志文件
  当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化。

1、内容:

  物理格式的日志,记录的是物理数据页面的修改的信息,其redo log是顺序写入redo log file的物理文件中去的。

2、redo log是什么时候写盘的?

是在事物开始之后逐步写盘的。

事务开始之后就产生redo log,redo log的写盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。(先将重做日志写入缓存,将日志缓冲区的日志刷新到磁盘,写入磁盘的方式有上面3种)

【注】即使某个事务还没有提交,Innodb存储引擎仍然每秒会将重做日志缓存刷新到重做日志文件。这一点是必须要知道的,因为这可以很好地解释再大的事务的提交(commit)的时间也是很短暂的。

3、什么时候释放:

  当对应事务的脏页写入到磁盘之后,redo log的使命也就完成了,重做日志占用的空间就可以重用(被覆盖)。

undo log

保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。(作用)

事务发生异常需要回滚,这时就需要回滚日志。回滚日志不同于重做日志,它是逻辑日志,对数据库的修改都逻辑的取消了。当事务回滚时,它实际上做的是与先前相反的工作。对于每个INSERT,InnoDB存储引擎都会完成一个DELETE;对于每个UPDATE,InnoDB存储引擎都会执行一个相反的UPDATE。

未提交的事务和回滚了的事务也会产生重做日志。InnoDB存储引擎会重做所有事务包括未提交的事务和回滚了的事务,然后通过回滚日志回滚那些未提交的事务。使用这种策略需要回滚日志在重做日志之前写入磁盘,使得持久化变得复杂起来。为了降低复杂度,InnoDB存储引擎将回滚日志作数据,记录回滚日志的操作也会记录到重做日志中。这样回滚日志就可以像数据一样缓存起来,而不用在重写日志之前写入磁盘了。

1、内容:

  逻辑格式的日志,在执行undo的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redo log的。

2、什么时候产生?

  事务开始之前,将当前是的版本生成undo log,undo 也会产生 redo 来保证undo log的可靠性

3、什么时候释放?

  当事务提交之后,undo log并不能立马被删除,而是放入待清理的链表,由purge线程判断是否由其他事务在使用undo段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间。

bin log

1、作用:

  用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。 用于数据库的基于时间点的还原。

2、内容:

逻辑格式的日志,可以简单认为就是执行过的事务中的sql语句。

  但又不完全是sql语句这么简单,而是包括了执行的sql语句(增删改)反向的信息,也就意味着delete对应着delete本身和其反向的insert;update对应着update执行前后的版本的信息;insert对应着delete和insert本身的信息。

在使用mysqlbinlog解析binlog之后一些都会真相大白。
因此可以基于binlog做到类似于oracle的闪回功能,其实都是依赖于binlog中的日志记录。

3、什么时候产生:

  事务提交的时候,一次性将事务中的sql语句(一个事物可能对应多个sql语句)按照一定的格式记录到binlog中。这里与redo log很明显的差异就是redo log并不一定是在事务提交的时候刷新到磁盘,redo log是在事务开始之后就开始逐步写入磁盘。

  因此对于事务的提交,即便是较大的事务,提交(commit)都是很快的,但是在开启了bin_log的情况下,对于较大事务的提交,可能会变得比较慢一些。这是因为binlog是在事务提交的时候一次性写入的造成的,这些可以通过测试验证。

4、什么时候释放:

binlog的默认是保持时间由参数expire_logs_days配置,也就是说对于非活动的日志文件,在生成时间超过expire_logs_days配置的天数之后,会被自动删除。

binlog与redolog的区别?

在MySQL数据库中还有一种二进制日志,其用来基于时间点的还原及主从复制。从表面上来看其和重做日志非常相似,都是记录了对于数据库操作的日志。但是,从本质上来看有着非常大的不同。 首先重做日志是在InnoDB存储引擎层产生的,而二进制日志是在MySQL数据库的上层产生的。其次,两种日志记录的内容形式不同。二进制日志是一种逻辑日志,其记录的是对应的SQL语句。而重做日志是物理日志,记录的是每个页的修改。此外,两种日志记录写入磁盘的时间点不同,二进制日志只在事务提交完成后进行一次写入,重做日志在事务进行时不断地写入。

Mysql的主从复制

好处

1、做数据的热备,作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。

2、架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。

3、读写分离,使数据库能支撑更大的并发。在报表中尤其重要。由于部分报表sql语句非常的慢,导致锁表,影响前台服务。如果前台使用master,报表使用slave,那么报表sql将不会造成前台锁,保证了前台速度。

原理

1.数据库有个bin-log二进制文件,记录了所有sql语句。

2.我们的目标就是把主数据库的bin-log文件的sql语句复制过来。

3.让其在从数据的relay-log重做日志文件中再执行一次这些sql语句即可。

4.下面的主从配置就是围绕这个原理配置

5.具体需要三个线程来操作:

1)binlog输出线程。每当有从库连接到主库的时候,主库都会创建一个线程然后发送binlog内容到从库。

在从库里,当复制开始的时候,从库就会创建两个线程进行处理:

2)从库I/O线程。当START SLAVE语句在从库开始执行之后,从库创建一个I/O线程,该线程连接到主库并请求主库发送binlog里面的更新记录到从库上。从库I/O线程读取主库的binlog输出线程发送的更新并拷贝这些更新到本地文件,其中包括relay log文件。

3)从库的SQL线程。从库创建一个SQL线程,这个线程读取从库I/O线程写到relay log的更新事件并执行。

可以知道,对于每一个主从复制的连接,都有三个线程。拥有多个从库的主库为每一个连接到主库的从库创建一个binlog输出线程,每一个从库都有它自己的I/O线程和SQL线程。

  • 步骤一:主库db的更新事件(update、insert、delete)被写到binlog

  • 步骤二:从库发起连接,连接到主库

  • 步骤三:此时主库创建一个binlog dump thread,把binlog的内容发送到从库

  • 步骤四:从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log

  • 步骤五:还会创建一个SQL线程,从relay log里面读取内容,从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db

计算机网络

OSI七层网络

tcp 三次握手过程以及为什么要三次握手

若建立连接只需两次握手,客户端并没有太大的变化,在获得服务端的应答后进入ESTABLISHED状态,即确认自己的发送和接受信息的功能正常.
如果服务端在收到连接请求后就进入ESTABLISHED状态,不能保证客户端能收到自己的信息,此时如果网络拥塞,客户端发送的连接请求迟迟到不了服务端,客户端便超时重发请求,如果服务端正确接收并确认应答,双方便开始通信,通信结束后释放连接。
此时,如果那个失效的连接请求抵达了服务端,由于只有两次握手,服务端收到请求就会进入ESTABLISHED状态,等待发送数据或主动发送数据。但此时的客户端早已进入CLOSED状态,服务端将会一直等待下去,这样浪费服务端连接资源.

第三次握手 ACK 丢失会发生什么

如果那个失效的连接请求抵达了服务端,由于只有两次握手,服务端收到请求就会进入ESTABLISHED状态,等待发送数据或主动发送数据。但此时的客户端早已进入CLOSED状态,服务端将会一直等待下去,这样浪费服务端连接资源.

tcp 四次挥手过程第四次握手完是直接关闭连接吗为什么进入 time-wait

不是,会等待2MSL

  • 原因一:保证TCP全双工连接的可靠释放
    解析:假设场景为客户端主动向服务器发起断开连接,假如在主动方(客户端)最后一次发送的ACK在网络中丢失,根据TCP的超时重传机制,被动方(服务器)需要重新向客户端发送FIN+ACK,在FIN未达到之前,必须维护这条连接;并且要接收到客户端发出的ACK确认后才能终止连接;如果直接在重传的FIN到达之前而关闭连接,当FIN到达后会促使客户端TCP传输层发送RST重新建立连接,而本质上这是一个正常断开连接的过程。

另一种解释是:
4次握手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来;若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。

  • 原因二:为了使就得数据包在网络中因过期而失效
    解析:假设没有time_wait状态时,A刚刚与B断开连接,C又以和A相同的ip和port和B建立起连接,TCP协议栈无法区分A和C是不同的连接, 这时,A连接发送的数据到达B之后会被B的TCP传输层当做当下正常的连接发来的数据进行处理,实际上这是上一条连接的脏数据;所以在time_wait等待2MSL(报文在网络最大生存时间),将此连接的数据全部收到并丢弃,才能保证这些数据不会造成错误;
    为什么是两个2MSL的原因:如果不到2MSL就断开连接,新连接又以旧连接相同的端口和ip连接服务器,就连接的重复数据报到达又会干扰第二个连接;

tcp 拥塞控制的方法

慢开始

1.慢开始不是指cwnd的增长速度慢(指数增长),而是指TCP开始发送设置cwnd=1。
2.思路:不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。这里用报文段的个数的拥塞窗口大小举例说明慢开始算法,实时拥塞窗口大小是以字节为单位的。如下图:

3.为了防止cwnd增长过大引起网络拥塞,设置一个慢开始门限(ssthresh状态变量)
当cnwd<ssthresh,使用慢开始算法
当cnwd=ssthresh,既可使用慢开始算法,也可以使用拥塞避免算法
当cnwd>ssthresh,使用拥塞避免算法

拥塞避免(按线性规律增长)

1.拥塞避免并非完全能够避免拥塞,是说在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。
2.思路:让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞控制窗口加一。

无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为1,执行慢开始算法。

快重传

1.快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。

2.由于不需要等待设置的重传计时器到期,能尽早重传未被确认的报文段,能提高整个网络的吞吐量。

快恢复(与快重传配合使用)

1.采用快恢复算法时,慢开始只在TCP连接建立时和网络出现超时时才使用。
2.当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半。但是接下去并不执行慢开始算法。
3.考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。

注意
发送方窗口的上限值=Min(接受窗口rwnd,拥塞窗口cwnd)
rwnd>cwnd 接收方的接收能力限制发送方窗口的最大值
rwnd<cwnd 网络的拥塞限制发送方窗口的最大值

知道哪些 http 状态码

HTTP状态码是当用户在浏览网页的时候,浏览器会返回一个http状态码,来响应浏览器的请求。

HTTP状态码一般是3位数。

常见HTTP状态码:

200:请求成功

301:资源(网页)别永久转移到其他URL

404:请求的资源不存在

500:服务器内部错误

HTTP状态码分类:

1**:服务端收到请求,要求请求方继续执行操作

100 继续,客户端继续请求

101 切换协议 服务器根据客户端的请求切换协议

2**:成功,操作被成功接收并处理

200 请求成功,一般用于get、post请求

201 已创建 请求成功并创建新的资源

202 已接受 请求已接受,但还没处理

204 接受了请求并处理,但是还没有返回内容

3**:重定向,需要进一步的操作以完成请求

301 端请求的网页别永久的移动到其他地方

​ 302 Found 临时性重定向。

4**:客户端错误

400 客户端语法错误

401 请求要求用户验证信息

403 禁止访问

404 找不到网页

5**:服务端错误

500 服务器内部错误

503 用于超载或系统维护,暂时无法处理客户端请求。

HTTPS工作原理

HTTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。TLS/SSL协议不仅仅是一套加密传输的协议,更是一件经过艺术家精心设计的艺术品,TLS/SSL中使用了非对称加密,对称加密以及HASH算法。握手过程的具体描述如下:

1.浏览器将自己支持的一套加密规则发送给网站。
2.网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器
。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。
3.浏览器获得网站证书之后浏览器要做以下工作
a) 验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。
b) 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。
c) 使用约定好的HASH算法计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。
4.网站接收浏览器发来的数据之后要做以下的操作:
a) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。
b) 使用密码加密一段握手消息,发送给浏览器。
5.浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密

这里浏览器与网站互相发送加密的握手消息并验证,目的是为了保证双方都获得了一致的密码,并且可以正常的加密解密数据,为后续真正数据的传输做一次测试。另外,HTTPS一般使用的加密与HASH算法如下:

非对称加密算法:RSA,DSA/DSS
对称加密算法:AES,RC4,3DES
HASH算法:MD5,SHA1,SHA256

HTTPS对应的通信时序图如下:

HTTPS协议和HTTP协议的区别: (具体HTTP协议的介绍可见参考资料2)
https协议需要到ca申请证书,一般免费证书很少,需要交费。
http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。
http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。
http的连接很简单,是无状态的 。
HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议, 要比http协议安全

服务端需要输入一个数字,客户端输入了一个字符串,返回什么状态码?

500

http头部?

HTTP Request Header 常见的请求头:

Accept:浏览器能够处理的内容类型

Accept-Charset:浏览器能够显示的字符集

Accept-Encoding:浏览器能够处理的压缩编码

Accept-Language:浏览器当前设置的语言

Connection:浏览器与服务器之间连接的类型

Cookie:当前页面设置的任何Cookie

Host:发出请求的页面所在的域

Referer:发出请求的页面的URL

User-Agent:浏览器的用户代理字符串

HTTP Responses Header 常见的响应头:

Date:表示消息发送的时间,时间的描述格式由rfc822定义

server:服务器名字

Connection:浏览器与服务器之间连接的类型

content-type:表示后面的文档属于什么MIME类型

Cache-Control:控制HTTP缓存

其中Content-type值常用的有以下几种:

application/x-www-form-urlencoded:form表单类型 ,浏览器的原生form表单
application/json:序列化后的 JSON 字符串,最常用,适合 RESTful 的接口
text/xml:是一种使用 HTTP 作为传输协议,XML 作为编码方式的远程调用规范
multipart/form-data:使用表单上传文件时,必须让 form 的 enctype 等于这个值

====================================================

a、通用首部字段(请求报文与响应报文都会使用的首部字段)
Date:创建报文时间
Connection:连接的管理
Cache-Control:缓存的控制
Transfer-Encoding:报文主体的传输编码方式
b、请求首部字段(请求报文会使用的首部字段)
Host:请求资源所在服务器
Accept:可处理的媒体类型
Accept-Charset:可接收的字符集
Accept-Encoding:可接受的内容编码
Accept-Language:可接受的自然语言
c、响应首部字段(响应报文会使用的首部字段)
Accept-Ranges:可接受的字节范围
Location:令客户端重新定向到的URI
Server:HTTP服务器的安装信息
d、实体首部字段(请求报文与响应报文的的实体部分使用的首部字段)
Allow:资源可支持的HTTP方法
Content-Type:实体主类的类型
Content-Encoding:实体主体适用的编码方式
Content-Language:实体主体的自然语言
Content-Length:实体主体的的字节数
Content-Range:实体主体的位置范围,一般用于发出部分请求时使用

get和post的区别

1、url可见性:

get,参数url可见;

post,url参数不可见

2、数据传输上:

get,通过拼接url进行传递参数;

post,通过body体传输参数

3、缓存性:

get请求是可以缓存的

post请求不可以缓存

4、后退页面的反应

get请求页面后退时,不产生影响

post请求页面后退时,会重新提交请求

5、传输数据的大小

get一般传输数据大小不超过2k-4k(根据浏览器不同,限制不一样,但相差不大)

post请求传输数据的大小根据php.ini 配置文件设定,也可以无限大。

6、安全性

这个也是最不好分析的,原则上post肯定要比get安全,毕竟传输参数时url不可见,但也挡不住部分人闲的没事在那抓包玩。安全性个人觉得是没多大区别的,防君子不防小人就是这个道理。对传递的参数进行加密,其实都一样。

==============================================

(1)post更安全(不会作为url的一部分,不会被缓存、保存在服务器日志、以及浏览器浏览记录中)
(2)post发送的数据更大(get有url长度限制)
(3)post能发送更多的数据类型(get只能发送ASCII字符)
(4)post比get慢
(5)post用于修改和写入数据,get一般用于搜索排序和筛选之类的操作(淘宝,支付宝的搜索查询都是get提交),目的是资源的获取,读取数据

一、为什么get比post更快
1.post请求包含更多的请求头
因为post需要在请求的body部分包含数据,所以会多了几个数据描述部分的首部字段(如:content-type),这其实是微乎其微的。

2.最重要的一条,post在真正接收数据之前会先将请求头发送给服务器进行确认,然后才真正发送数据
post请求的过程:
(1)浏览器请求tcp连接(第一次握手)
(2)服务器答应进行tcp连接(第二次握手)
(3)浏览器确认,并发送post请求头(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
(4)服务器返回100 Continue响应
(5)浏览器发送数据
(6)服务器返回200 OK响应
get请求的过程:
(1)浏览器请求tcp连接(第一次握手)
(2)服务器答应进行tcp连接(第二次握手)
(3)浏览器确认,并发送get请求头和数据(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
(4)服务器返回200 OK响应

3.get会将数据缓存起来,而post不会
可以做个简短的测试,使用ajax采用get方式请求静态数据(比如html页面,图片)的时候,如果两次传输的数据相同,第二次以后消耗的时间将会在10ms以内(chrome测试),而post每次消耗的时间都差不多。经测试,chrome和firefox下如果检测到get请求的是静态资源,则会缓存,如果是数据,则不会缓存,但是IE什么都会缓存起来。

二、get传参最大长度的理解误区
1.总结
(1)http协议并未规定get和post的长度限制
(2)get的最大长度限制是因为浏览器和web服务器限制了URL的长度
(3)不同的浏览器和web服务器,限制的最大长度不一样
(4)要支持IE,则最大长度为2083byte,若支持Chrome,则最大长度8182byte

http为什么用到tcp,tcp怎么做到可靠?

TCP协议保证数据传输可靠性的方式主要有:

  • 校验和
  • 序列号
  • 确认应答
  • 超时重传
  • 连接管理
  • 流量控制
  • 拥塞控制

校验和

计算方式:在数据传输的过程中,将发送的数据段都当做一个16位的整数。将这些整数加起来。并且前面的进位不能丢弃,补在后面,最后取反,得到校验和。
发送方:在发送数据之前计算检验和,并进行校验和的填充。
接收方:收到数据后,对数据以同样的方式进行计算,求出校验和,与发送方的进行比对

注意:如果接收方比对校验和与发送方不一致,那么数据一定传输有误。但是如果接收方比对校验和与发送方一致,数据不一定传输成功。

确认应答与序列号

序列号:TCP传输时将每个字节的数据都进行了编号,这就是序列号。
确认应答:TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文。这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。

序列号的作用不仅仅是应答的作用,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据。这也是TCP传输可靠性的保证之一。

超时重传

如果发送方发送完数据后,迟迟没有等到接收方的ACK报文,这该怎么办呢?而没有收到ACK报文的原因可能是什么呢?

首先,发送方没有介绍到响应的ACK报文原因可能有两点:

1)数据在传输过程中由于网络原因等直接全体丢包,接收方根本没有接收到。
2)接收方接收到了响应的数据,但是发送的ACK报文响应却由于网络原因丢包了。

TCP在解决这个问题的时候引入了一个新的机制,叫做超时重传机制。简单理解就是发送方在发送完数据后等待一个时间,时间到达没有接收到ACK报文,那么对刚才发送的数据进行重新发送。如果是刚才第一个原因,接收方收到二次重发的数据后,便进行ACK应答。如果是第二个原因,接收方发现接收的数据已存在(判断存在的根据就是序列号,所以上面说序列号还有去除重复数据的作用),那么直接丢弃,仍旧发送ACK应答。
那么发送方发送完毕后等待的时间是多少呢?如果这个等待的时间过长,那么会影响TCP传输的整体效率,如果等待时间过短,又会导致频繁的发送重复的包。如何权衡?

由于TCP传输时保证能够在任何环境下都有一个高性能的通信,因此这个最大超时时间(也就是等待的时间)是动态计算的。

在Linux中(BSD Unix和Windows下也是这样)超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。重发一次后,仍未响应,那么等待2 * 500ms的时间后,再次重传。等待4 * 500ms的时间继续重传。以一个指数的形式增长。累计到一定的重传次数,TCP就认为网络或者对端出现异常,强制关闭连接。

连接管理

连接管理就是三次握手与四次挥手的过程,在前面详细讲过这个过程,这里不再赘述。保证可靠的连接,是保证可靠性的前提

流量控制

接收端在接收到数据后,对其进行处理。TCP根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。

在TCP协议的报头信息当中,有一个16位字段的窗口大小。在介绍这个窗口大小时我们知道,窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。这个数字越大,证明接收端接收缓冲区的剩余空间越大,网络的吞吐量越大。接收端会在确认应答发送ACK报文时,将自己的即时窗口大小填入,并跟随ACK报文一起发送过去。而发送方根据ACK报文里的窗口大小的值的改变进而改变自己的发送速度。如果接收到窗口大小的值为0,那么发送方将停止发送数据。并且启动持续计时器,定期的向接收端发送窗口探测数据段,让接收端把窗口大小告诉发送端。

注:16位的窗口大小最大能表示65535个字节(64K),但是TCP的窗口大小最大并不是64K。在TCP首部中40个字节的选项中还包含了一个窗口扩大因子M,实际的窗口大小就是16为窗口字段的值左移M位。每移一位,扩大两倍

拥塞控制

前面有

拥塞控制与流量控制的区别

  • 流量控制:是端到端的控制,例如A通过网络给B发数据,A发送的太快导致B没法接收(B缓冲窗口过小或者处理过慢),这时候的控制就是流量控制,原理是通过滑动窗口的大小改变来实现。

概念:流量控制用于防止在端口阻塞的情况下丢帧,这种方法是当发送或接收缓冲区开始溢出时通过将阻塞信号发送回源地址实现的。流量控制可以有效的防止由于网络中瞬间的大量数据对网络带来的冲击,保证用户网络高效而稳定的运行。

  • 拥塞控制:是A与B之间的网络发生堵塞导致传输过慢或者丢包,来不及传输。防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不至于过载。拥塞控制是一个全局性的过程,涉及到所有的主机、路由器,以及与降低网络性能有关的所有因素。

概念:网络拥塞现象是指到达通信子网中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象。拥塞控制是处理网络拥塞现象的一种机制。

TCP拆包粘包

原因:

1.应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。

2.应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。

3.进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。

4.接收方法不及时读取套接字缓冲区数据,这将发生粘包。

解决办法:

1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

udp怎么实现可靠传输?

UDP它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

     传输层无法保证数据的可靠传输,只能通过 应用层 来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。
     实现确认机制、重传机制、窗口确认机制。
     
     如果你不利用linux协议栈以及上层socket机制,自己通过抓包和发包的方式去实现可靠性传输,那么必须实现如下功能:

     发送:包的分片、包确认、包的重发

     接收:包的调序、包的序号确认

     目前有如下开源程序利用udp实现了可靠的数据传输。分别为RUDP、RTP、UDT。

RUDP 使用类似于 TCP 的重发机制和拥塞控制算法

RTP提供的各种服务包括有效负载识别,序列编号,时间戳和投递监听。RTP能够序列化包,当这些包在收端不是按顺序到达的时。序列号也能被用来识别包丢失。时间戳被用于媒体有效的播放。到达的数据一直被RTCP监听,以通知RTP层来校正其编码和传输的参数

总结:简单来讲,要使用UDP来构建可靠的面向连接的数据传输,就要实现类似于TCP协议的超时重传,有序接受,应答确认,滑动窗口流量控制等机制,等于说要在传输层的上一层(或者直接在应用层)实现TCP协议的可靠数据传输机制,比如使用UDP数据包+序列号,UDP数据包+时间戳等方法,在服务器端进行应答确认机制,这样就会保证不可靠的UDP协议进行可靠的数据传输,

TCP UDP深挖 场景使用 特定场景下的选择

DNS解析过程

DNS( Domain Name System)是“域名系统

  1. 浏览器先检查自身缓存中有没有被解析过的这个域名对应的ip地址,如果有,解析结束。同时域名被缓存的时间也可通过TTL属性来设置。

  2. 如果浏览器缓存中没有(专业点叫还没命中),浏览器会检查操作系统缓存中有没有对应的已解析过的结果。而操作系统也有一个域名解析的过程。在windows中可通过c盘里一个叫hosts的文件来设置,如果你在这里指定了一个域名对应的ip地址,那浏览器会首先使用这个ip地址。

    但是这种操作系统级别的域名解析规程也被很多黑客利用,通过修改你的hosts文件里的内容把特定的域名解析到他指定的ip地址上,造成所谓的域名劫持。所以在windows7中将hosts文件设置成了readonly,防止被恶意篡改。

    中间插一个路由器缓存

  3. 如果至此还没有命中域名,才会真正的请求本地域名服务器(LDNS)来解析这个域名,这台服务器一般在你的城市的某个角落,距离你不会很远,并且这台服务器的性能都很好,一般都会缓存域名解析结果,大约80%的域名解析到这里就完成了。

  4. 如果LDNS仍然没有命中,就直接跳到Root Server 域名服务器请求解析

    • 根服务器拿到这个请求后,知道他是com.这个顶级域名下的,所以就会返回com域中的NS记录,一般来说是13台主机名和IP。

    • 然后ISPDNS向其中一台再次发起请求,com域的服务器发现你这请求是baidu.com这个域的,我一查发现了这个域的NS,那我就返回给你,你再去查。

    (目前百度有4台baidu.com的顶级域名服务器)。

    • ISPDNS不厌其烦的再次向baidu.com这个域的权威服务器发起请求,baidu.com收到之后,查了下有www的这台主机,就把这个IP返回给你了

      等价于以下的几步操作

  5. 根域名服务器返回给LDNS一个所查询域的主域名服务器(gTLD Server,国际顶尖域名服务器,如.com .cn .org等)地址

  6. 此时LDNS再发送请求给上一步返回的gTLD

  7. 接受请求的gTLD查找并返回这个域名对应的Name Server的地址,这个Name Server就是网站注册的域名服务器

  8. Name Server根据映射关系表找到目标ip,返回给LDNS

  9. LDNS缓存这个域名和对应的ip

  10. LDNS把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结束

上述图片是查找www.google.com的IP地址过程。首先在本地域名服务器中查询IP地址,如果没有找到,本地域名服务器会向根域名服务器发送一个请求,如果根域名服务器也不存在该域名时,本地域名会向com顶级域名服务器发送一个请求,依次类推下去。直到最后本地域名服务器得到google的IP地址并把它缓存到本地,供下次查询使用。

从上述过程中,可以看出网址的解析是一个从右向左的过程: com -> google.com -> www.google.com。但是你是否发现少了点什么,根域名服务器的解析过程呢?事实上,真正的网址是www.google.com.,并不是我多打了一个.,这个.对应的就是根域名服务器,默认情况下所有的网址的最后一位都是.,既然是默认情况下,为了方便用户,通常都会省略,浏览器在请求DNS的时候会自动加上,所有网址真正的解析过程为:. -> .com -> google.com. -> www.google.com.

一个URL到页面加载全过程

从输入URL到页面加载的主干流程如下:

1、DNS解析
2、建立TCP连接
3、发送HTTP请求
4、接收处理请求,服务器进行处理并返回HTTP报文
5、浏览器解析并渲染页面
6、关闭连接

1、DNS解析

上面有

2、建立TCP连接

上面有

3、发送HTTP请求

发送HTTP请求的过程就是构建HTTP请求报文并通过TCP协议中发送到服务器指定端口(HTTP协议80/8080, HTTPS协议443)。HTTP请求报文是由三部分组成: 请求行, 请求报头和请求正文。

请求行

格式如下:
Method Request-URL HTTP-Version CRLF

eg: GET index.html HTTP/1.1
常用的方法有: GET, POST, PUT, DELETE, OPTIONS, HEAD。

请求报头

请求报头允许客户端向服务器传递请求的附加信息和客户端自身的信息。
PS: 客户端不一定特指浏览器,有时候也可使用Linux下的CURL命令以及HTTP客户端测试工具等。
常见的请求报头有: Accept, Accept-Charset, Accept-Encoding, Accept-Language, Content-Type, Authorization, Cookie, User-Agent等。

请求正文

当使用POST, PUT等方法时,通常需要客户端向服务器传递数据。这些数据就储存在请求正文中。在请求包头中有一些与请求正文相关的信息,例如: 现在的Web应用通常采用Rest架构,请求的数据格式一般为json。这时就需要设置Content-Type: application/json

4、接收处理请求,服务器进行处理并返回HTTP报文

从在固定的端口接收到TCP报文开始,这一部分对应于编程语言中的socket。它会对TCP连接进行处理,对HTTP协议进行解析,并按照报文格式进一步封装成HTTP Request对象,供上层使用。这一部分工作一般是由Web服务器去进行,Web服务器有Tomcat, Jetty和Netty等等。

HTTP响应报文也是由三部分组成: 状态码, 响应报头和响应报文。

状态码

状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:

1xx:指示信息–表示请求已接收,继续处理。

2xx:成功–表示请求已被成功接收、理解、接受。

3xx:重定向–要完成请求必须进行更进一步的操作。

4xx:客户端错误–请求有语法错误或请求无法实现。

5xx:服务器端错误–服务器未能实现合法的请求。

响应报头

常见的响应报头字段有: Server, Connection…

响应报文

服务器返回给浏览器的文本信息,通常HTML, CSS, JS, 图片等文件就放在这一部分。

5、浏览器解析并渲染页面

浏览器是一个边解析边渲染的过程。首先浏览器解析HTML文件构建DOM树,然后解析CSS文件构建渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上。这个过程比较复杂,涉及到两个概念: reflow(回流)和repain(重绘)。DOM节点中的各个元素都是以盒模型的形式存在,这些都需要浏览器去计算其位置和大小等,这个过程称为relow;当盒模型的位置,大小以及其他属性,如颜色,字体,等确定下来之后,浏览器便开始绘制内容,这个过程称为repain。页面在首次加载时必然会经历reflow和repain。reflow和repain过程是非常消耗性能的,尤其是在移动设备上,它会破坏用户体验,有时会造成页面卡顿。所以我们应该尽可能少的减少reflow和repain。

JS的解析是由浏览器中的JS解析引擎完成的。JS是单线程运行,也就是说,在同一个时间内只能做一件事,所有的任务都需要排队,前一个任务结束,后一个任务才能开始。但是又存在某些任务比较耗时,如IO读写等,所以需要一种机制可以先执行排在后面的任务,这就是:同步任务(synchronous)和异步任务(asynchronous)。JS的执行机制就可以看做是一个主线程加上一个任务队列(task queue)。同步任务就是放在主线程上执行的任务,异步任务是放在任务队列中的任务。所有的同步任务在主线程上执行,形成一个执行栈;异步任务有了运行结果就会在任务队列中放置一个事件;脚本运行时先依次运行执行栈,然后会从任务队列里提取事件,运行任务队列中的任务,这个过程是不断重复的,所以又叫做事件循环(Event loop)。

浏览器在解析过程中,如果遇到请求外部资源时,如图像,iconfont,JS等。浏览器将重复1-6过程下载该资源。请求过程是异步的,并不会影响HTML文档进行加载,但是当文档加载过程中遇到JS文件,HTML文档会挂起渲染过程,不仅要等到文档中JS文件加载完毕还要等待解析执行完毕,才会继续HTML的渲染过程。原因是因为JS有可能修改DOM结构,这就意味着JS执行完成前,后续所有资源的下载是没有必要的,这就是JS阻塞后续资源下载的根本原因。CSS文件的加载不影响JS文件的加载,但是却影响JS文件的执行。JS代码执行前浏览器必须保证CSS文件已经下载并加载完毕。

6、关闭连接

TCP四次挥手上面有

http协议,还说了个自定制,讨论了好久的自定制协议

http和https的区别

多加了ssl加密的功能

https的设计目标

1)数据保密性:不被第三方看到

2)数据完整性:及时发现第三方篡改的内容

3)身份校验安全性:保证数据到达用户期望的目的地

区别

1、HTTPS 协议需要到 CA (Certificate Authority,证书颁发机构)申请证书,一般免费证书较少,因而需要一定费用。

证书包含的信息:1)证书的过期时间与序列号 2)所有者的姓名等信息。 3)所有者的

2、HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。

3、HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

4、HTTP 的连接很简单,是无状态的。HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。)

https协议的改进

1)双向的身份验证

2)数据传输的机密性

3)防止重放攻击

https优点

相对安全

https缺点

1、HTTPS 协议握手阶段比较费时,会使页面的加载时间延长近。

2、HTTPS 连接缓存不如 HTTP 高效,会增加数据开销,甚至已有的安全措施也会因此而受到影响。

3、HTTPS 协议的安全是有范围的,在黑客攻击、拒绝服务攻击和服务器劫持等方面几乎起不到什么作用。

4、SSL 证书通常需要绑定 IP,不能在同一 IP 上绑定多个域名,IPv4 资源不可能支撑这个消耗。

5、成本增加。部署 HTTPS 后,因为 HTTPS 协议的工作要增加额外的计算资源消耗,例如 SSL 协议加密算法和 SSL 交互次数将占用一定的计算资源和服务器成本。

6、HTTPS 协议的加密范围也比较有限。最关键的,SSL 证书的信用链体系并不安全,特别是在某些国家可以控制 CA 根证书的情况下,中间人攻击一样可行。

HTTPS连接过程

证书验证部分采用的是非对称加密,信息传输部分采用的是对称加密

① 客户端的浏览器向服务器发送请求,并传送客户端 SSL 协议的版本号,加密算法的种类,产生的随机数,以及其他服务器和客户端之间通讯所需要的各种信息。

② 服务器向客户端传送 SSL 协议的版本号,加密算法的种类,随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。

③ 客户端利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过期,发行服务器证书的 CA 是否可靠,发行者证书的公钥能否正确解开服务器证书的 “发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配。如果合法性验证没有通过,通讯将断开;如果合法性验证通过,将继续进行第四步。

④ 用户端随机产生一个用于通讯的 “对称密码”,然后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中获得)对其加密,然后将加密后的“预主密码”传给服务器。

⑤ 如果服务器要求客户的身份认证(在握手过程中为可选),用户可以建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户自己的证书以及加密过的密钥一起传给服务器。

⑥ 如果服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的 CA 是否可靠,发行 CA 的公钥能否正确解开客户证书的发行 CA 的数字签名,检查客户的证书是否在证书废止列表(CRL)中。检验如果没有通过,通讯立刻中断;如果验证通过,服务器将用自己的私钥解开加密的私钥,然后执行一系列步骤来产生主通讯密码(客户端也将通过同样的方法产生相同的主通讯密码)。

⑦ 服务器和客户端用相同的对称加密密钥,对称密钥用于 SSL 协议的安全数据通讯的加解密通讯。同时在 SSL 通讯过程中还要完成数据通讯的完整性,防止数据通讯中的任何变化。

⑧ 客户端向服务器端发出信息,指明后面的数据通讯将使用的步骤 ⑦ 中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。

⑨ 服务器向客户端发出信息,指明后面的数据通讯将使用的步骤 ⑦ 中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。

⑩ SSL 的握手部分结束,SSL 安全通道的数据通讯开始,客户和服务器开始使用相同的对称密钥进行数据通讯,同时进行通讯完整性的检验。

如何证明浏览器收到的公钥一定是该网站的公钥?

证书 + 数字签名

总结起来就是说CA机构颁布的证书保证了公钥的安全,里面的数字签名可以进行对比来判断是否被篡改

数字签名的制作过程:

  1. CA拥有非对称加密的私钥和公钥。
  2. CA对证书明文信息进行hash。
  3. 对hash后的值用私钥加密,得到数字签名。

明文和数字签名共同组成了数字证书,这样一份数字证书就可以颁发给网站了。
那浏览器拿到服务器传来的数字证书后,如何验证它是不是真的?(有没有被篡改、掉包)

浏览器验证过程:

  1. 拿到证书,得到明文T,数字签名S。
  2. 用CA机构的公钥对S解密(由于是浏览器信任的机构,所以浏览器保有它的公钥。详情见下文),得到S’。
  3. 用证书里说明的hash算法对明文T进行hash得到T’。
  4. 比较S’是否等于T’,等于则表明证书可信。

http使用的方法

get,post,header,put,delete

Post是2个TCP,为什么比Get多一个TCP

Post有个状态码为100的情况,如果获取100失败,那么就不用发送第二次了

对称加密和非对称加密的实际使用场景,并判断经典使用场景

对称密钥:一般用于对效率有要求的实时数据加密通信。比如在使用 VPN 或者代理进行 加密通信时,既要保证数据的保密性,又要保证不能有高的延迟,所以通常会使用对称加密算法。

非对称密钥:主要用于秘钥交换,证书等场景,安全性更高

Java

继承和多态

重写和重载

对多态性的体现不同

  1. 重载体现的是编译多态性
  2. 重写体现的是运行多态性

规则不同

​ 重载:

  1. 对象:同一个类的不同方法中

  2. 参数必须不同,可以是类型,也可以是顺序

  3. 不规定返回值类型必须一样

  4. 可以有不同的权限修饰符

  5. 方法名必须一样,才能称方法与方法之间构成重载

  6. 可以抛出任意大小的异常

    重写:

  7. 前提:重写(覆盖)必须发生在子父类之间,且只能是子类对父类的方法进行重写

  8. 参数列表必须和父类的一样

  9. 返回值类型需一致

  10. 权限修饰符子类需大于或等于父类方法权限修饰

  11. 子类抛的异常需不能大于父类抛出异常

重载的方法不能根据返回类型进行区分

String、StringBuffer、StringBuilder

  1. 可变与不可变

    1)String是内容不可变的字符串。String底层使用了一个不可变的字符数组(final char[])。
      

    2)StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串(没有使用final来修饰),如下就是,可知这两种对象都是可变的。

  1. 线程安全性

    1)String中的对象是不可变的,也就可以理解为常量,显然线程安全。

    2)StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。

    3)StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。

Java和C++的区别

同:都是面向对象的语言,支持封装、继承、多态

异:1.Java不提供指针操作内存,程序内存更加安全

2.Java的类是单继承的,C++可以多继承;虽然类不能多继承,但是接口可以多实现

3.Java有自动内存管理机制,不需要程序员手动释放无用内存

==与equals


它的作用是判断两个对象的地址是否相等。判断是不是同一个对象(基本数据类型的
是比较值,引用数据类型==比较的是内存地址)

equals:
它的作用是判断两个对象是否相等,一般都有两种情况:

情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”来比较对象。

情况2:类覆盖了equals()方法。一般,我们都会覆盖equals()方法来比较两个对象的内容是否相等。若他们相等会返回true。

说明:

1.String中得到equals方法是被重写的,因为Object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。

2.当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String.

hashCode与equals

hashcode只是用来缩小查找成本。hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个Int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。HashCode()是定义在Object类中的,所以涉及到对象我们就会用到这个方法,而且这个方法时本地方法,是用C或C++实现的,该方法通常用来把对象的物理地址转换为整数之后返回

为什么重写equals时必须重写hashCode方法?

如果两个对象相等,则hashcode一定也是相同的。两个对象相等,对两个对象分别调用equals方法都返回true。但是,**两个对象有相同的hashcode值,他们也不一定是相等的。**因此equals方法被覆盖过,则hashCode方法也必须被覆盖。这样才能保证相等

类加载机制

JVM 的类加载机制是指 JVM 把描述类的数据从 .class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是 JVM 的类加载机制。

类的生命周期总共分为7个阶段:加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备、解析三个步骤又可统称为连接。
加载、验证、准备、初始化和卸载五个步骤的顺序都是确定的,解析阶段在某些情况下有可能发生在初始化之后,这是为了支持 Java 语言的运行期绑定的特性
在 JVM 虚拟机规范中并没有规定加载的时机,但是却规定了初始化的时机,而加载、验证、准备三个步骤是在初始化之前

加载

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口

总结:把代码数据加载到内存中

验证

当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。这个校验过程大致可以分为下面几个类型:

  • **JVM规范校验。**JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。例如:文件是否是以 0x cafe bene开头,主次版本号是否在当前虚拟机处理范围之内等。
  • **代码逻辑校验。**JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果,但是最后却没有返回结果。代码中引用了一个名为 Apple 的类,但是你实际上却没有定义 Apple 类。

=========================================================

确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 文件格式验证:验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
  • 字节码验证:通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
  • 符号引用验证:发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。

准备(重点)

当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。

  • **内存分配的对象。**Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

  • **初始化的类型。**在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。

    public static final int number = 3;
    

解析

当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

初始化(重点)

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。

卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存

JVM内存模型

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中 保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

指令重排

public class PossibleReordering { 
		static int x = 0, y = 0; 
		static int a = 0, b = 0; 
		public static void main(String[] args) throws InterruptedException { 
				Thread one = new Thread(new Runnable() { 
							public void run() { 
									a = 1; x = b; 
							} 
				});
				Thread other = new Thread(new Runnable() { 
							public void run() {
              		b = 1; y = a; 
              } 
        }); 
        one.start();
        other.start(); 
        one.join();
        other.join(); 
        System.out.println(“(” + x + “,” + y + “)”);
    }
}

运行结果可能为(1,0)、(0,1)或(1,1),也可能是(0,0)。因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待

对于单线程来说,不管发生怎样的重排,都必须保持与源代码一致的输出结果(As-If-Serial). 上述规则保证了单线程的执行结果总是与预期一致,但在多线程的情况,就会出现与预期不一致的情况, * 而导致这一情况发生的原因,正是指令重排

内存屏障

内存屏障正是通过阻止屏障两边的指令重排序来避免编译器和硬件的不正确优化而提出的一种解决办法

volatile作用能保证可见性和防止指令重排序

volatile与synchronized对比
volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度

happen-before

JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见

具体的定义为:
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

双亲委派机制

如果说自定义一些公共类,比如String,Object等,没有双亲委派机制的话,类的唯一性就没法得到保证,会使得防止内存中出现多份同样的字节码。

Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader、User ClassLoader

  • BootstrapClassLoader(启动类加载器):主要负责加载核心的类库(java.lang.*等),JVM_HOME/lib目录下的,构造ExtClassLoader和APPClassLoader。
  • ExtClassLoader (拓展类加载器):主要负责加载jre/lib/ext目录下的一些扩展的jar
  • AppletClassLoader(系统类加载器):主要负责加载应用程序的主函数类
  • 自定义类加载器:主要负责加载应用程序的主函数类

缺陷:jdk中的基础类作为用户典型的api被调用,但是也存在被api调用用户的代码的情况,典型的如SPI代码

解决方案:1)自定义类加载器, 继承ClassLoader类,重写loadClass、findClass

2)使用线程上下文类加载器

类加载详细过程

双亲委派模型的工作流程:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,只有当父类加载器无法完成这个类加载请求时,才会让子类加载器去处理这个请求。

JVM

核心参数

1、-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=50m 元空间的

2、-Xms:Java堆内存的大小

3、-Xmx:Java堆内存的最大大小

4、-Xmn:Java堆内存中的年轻代大小,扣除年轻代剩下的就是老年代的内存大小

5、-Xss:每个线程的栈内存大小

每个部分主要存放的内容

垃圾回收机制

垃圾判断方法

引用计数法

给每个对象添加一个计数器,当有地方引用该对象时计数器加1,当引用失效时计数器减1。用对象计数器是否为0来判断对象是否可被回收。缺点:无法解决循环引用的问题

可达性分析算法

通过GC ROOT的对象作为搜索起始点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可被回收(可作为GC ROOT的对象:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象)

通过可达性分析,那些不可达的对象并不是立即被销毁,他们还有被拯救的机会。
如果要回收一个不可达的对象,要经历两次标记过程。首先是第一次标记,并判断对象是否覆写了 finalize 方法,如果没有覆写,则直接进行第二次标记并被回收。如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。

虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象

强引用,软引用,弱引用,虚引用

无论是通过引用计数法判断引用数量,还是通过可达性分析判断对象的引用是否可大,判 断对象的存活都与”引用”有关。

强引用

如果一个对象具有强引用,对于我们来说是不能缺少的对象垃圾回收器绝不会回收它。

当内存空间不足,JVM 宁可抛出异常也不会回收它。例子:list 集合里的数据不会释放,即使内存不足也不

软引用

软引用的对象是可有可无的对象。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用

弱引用

弱引用的对象也是可有可无的对象。弱引用于软引用的区别在于:垃圾回收器在扫描他所 管辖的内存区域,一旦发现弱引用的对象,不管内存是否足够,都会回收它的内存。不过垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象

虚引用

虚引用并不会决定对象的生命周期。他跟没有任何引用一样,在任何时候都可能被垃圾回收

垃圾回收算法

标记-清除算法

存在内存碎片、浪费可使用空间

复制算法

浪费了一半内存空间

标记-整理算法

标记整理算法解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。标记整理算法对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。

分代收集算法

根据对象存活周期的不同将内存划分为几块。

  • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
  • 在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理算法或者标记-整理算法来进行回收。

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC

新生代:目的是回收那些生命周期短的对象,主要存放新产生的对象。新生代按照8:1:1分为eden区、survivor0、survivor1,大部分对象在eden区中生成,当eden满时,将存活的对象复制到survivor0,然后清空eden,当eden、survivor0都满了时,将这两个区中存活的对象复制到survivor1,然后清空eden、survivor0,当着三个区都满了时则把存货对象复制到老年代,如果老年代也满了则触发FullGC。新生代的全回收叫MinorGC,MinorGC发生频率比较高,不一定等到新生代满了时才进行。

老年代:存放对象生命周期较长,且内存大概是新生代的两倍,老年代存活对象生命周期长,因此MajorGC发生频率较低。

回收策略

1、对象优先在Eden分配

2、大对象直接进入老年代

3、长期存活的对象将进入老年代

4、动态对象年龄判

常见的垃圾收集器

Serial收集器(复制算法)

单线程、stop the world

Serial Old收集器(标记-整理算法)

Serial的老年代版本、单线程、

ParNew收集器(复制算法)

Serial的多线程版本、 stop the world

Parallel Scavenge收集器(复制算法)

新生代收集器、并发多线程

Parallel Old收集器(复制算法)

Parallel Scavenge的老年代版本、多线程

CMS(Concurrent Mark Sweep)收集器(标记-清除算法)

以获取最短回收停顿时间为目标、 运行过程:1)初识标记 2)并发标记 3)重新标记 4) 并发清除

会产生大量内存碎片

  1. 初始标记 (Stop the World事件 CPU停顿, 很短) 初始标记仅标记一下GC Roots能直接关联到的对象,速度很快;

  2. 并发标记 (收集垃圾跟用户线程一起执行, 不进行stop the world) 并发标记过程就是进行GC Roots Tracing的过程;

  3. 重新标记 (Stop the World事件 CPU停顿,比初始标记稍微长,远比并发标记短)修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记时间短

  4. 并发清理 -清除算法 (不进行stop the world);

整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的

G1收集器(标记-整理算法)

运行过程:1)初始标记 2)并发标记 3)最终标记 4)筛选标记 不会产生内存碎片。可以精确控制停顿

1、初始标记(stop the world事件 CPU停顿只处理垃圾);

2、并发标记(与用户线程并发执行);(不会触发stop the world事件)

3、最终标记(stop the world事件 ,CPU停顿处理垃圾);

4、筛选回收(stop the world事件 根据用户期望的GC停顿时间回收); (注意:CMS 在这一步不需要stop the world)

JVM GC核心参数

1、-XX:NewRatio 是年老代 新生代相对的比例,比如NewRatio=2,表明年老代是新生代的2倍。老年代占了heap的2/3,新生代占了1/3

2、-XX:SurvivorRatio 配置的是在新生代里面Eden和一个Servive的比例

如果指定NewRatio还可以指定NewSizeMaxNewSize,如果同时指定了会如何???

NewRatio=2,这个时候新生代会尝试分配整个Heap大小的1/3的大小,但是分配的空间不会小于-XX:NewSize也不会大于 –XX:MaxNewSize

3、-XX:NewSize –XX:MaxNewSize

实际设置比例还是设置固定大小,固定大小理论上速度更高。

-XX:NewSize –XX:MaxNewSize理论越大越好,但是整个Heap大小是有限的,一般年轻代的设置大小不要超过年老代。

4、-XX:SurvivorRatio新生代里面Eden和一个Servive的比例,如果SurvivorRatio是5的话,也就是Eden区域是SurviveTo区域的5倍。Survive由From和To构成。结果就是整个Eden占用了新生代5/7,From和To分别占用了1/7,如果分配不合理,Eden太大,这样产生对象很顺利,但是进行GC有一部分对象幸存下来,拷贝到To,空间小,就没有足够的空间,对象会被放在old Generation中。如果Survive空间大,会有足够的空间容纳GC后存活的对象,但是Eden区域小,会被很快消耗完,这就增加了GC的次数。

5、-XX:MaxTenuringThreshold 垃圾最大存活年龄

调优策略

物理内存一定的条件下,新生代设置越大,老年代就越小,Full GC 频率越高,但是 Full GC 时间越短;相反新生代设置越小,老年代就越大,Full GC 频率就越低,但每次 Full GC 消耗的时间越大

1)-Xms 和-Xmx 的值设置一样,为了避免内存的动态调整,因为当空闲堆内存不同的时候, 会切换-Xms 和-Xmx 内存状态,如果设置一样的话,就不会进行动态内存调整,节约资源。
2)新生代尽量设置大一些,让对象在新生代多存活一段时间,每次 Minor GC 进可能多收集垃圾对象,防止进入老年代,进行 Full GC.
3)老年代如果使用 CMS 收集器,新生代可以不用太大,因为 CMS 的并发收集速度也很快, 收集过程可以与用户线程并发执行。

避免以下问题(避免不需要的对象进入老年代):
1)避免创建过大的对象或者数组:过大的对象和数组在新生代没有足够的空间会进入,老年代,会提前出发 FullGC
2)避免同时加载大量数据:从数据库或者 Excel 取大量数据,尽量分批读取
3)当程序中有对象引用,如果使用完后,尽量设置为 null,比如 obj1=null。避免这些对象进入老年代
4)避免长时间等待外部资源,缩小对象的生命周期,避免进入老年代

打印线程栈信息

原子类了解吗? Cas

java.util.concurrent.atomic 下的类,就是具有原子性的类,可以原子性地执行添加、递增、递减等操作。比如之前多线程下的线程不安全的 i++ 问题,到了原子类这里,就可以用功能相同且线程安全的 getAndIncrement 方法来优雅地解决。

作用

原子类的作用和锁有类似之处,是为了保证并发情况下线程安全。不过原子类相比于锁,有一定的优势:

1)粒度更细:原子变量可以把竞争范围缩小到变量级别,通常情况下,锁的粒度都要大于原子变量的粒度。
2)效率更高:除了高度竞争的情况之外,使用原子类的效率通常会比使用同步互斥锁的效率更高,因为原子类底层利用了 CAS 操作,不会阻塞线程

CAS(Compare And Swap)

比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较知道主内存和工作内存中的值一直为止

  1. atomicInteger.getAndIncrement();

        public final int getAndIncrement() {
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }
    

Unsafe

  • 是CAS核心类,由于Java方法无法直接访问地层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

    Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

  • 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的

  • 变量value用volatile修饰,保证多线程之间的可见性

//unsafe.getAndAddInt
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

var1 AtomicInteger对象本身

var2 该对象的引用地址

var4 需要变动的数据

var5 通过var1 var2找出的主内存中真实的值

用该对象前的值与var5比较;

如果相同,更新var5+var4并且返回true,

如果不同,继续去之然后再比较,直到更新完成

反射的底层实现?

含义

反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

实现方式

获取Class对象,有4种方法:

(1)Class.forName(“类的路径”);

(2)类名.class;

(3)对象名.getClass();

(4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象。

实现反射的类

(1)Class:表示正在运行的Java应用程序中的类和接口,注意所有获取对象的信息都需要Class类来实现;

(2)Field:提供有关类和接口的属性信息,以及对它的动态访问权限;

(3)Constructor:提供关于类的单个构造方法的信息以及它的访问权限;

(4)Method:提供类或接口中某个方法的信息。

优缺点

优点(1)能够运行时动态获取类的实例,提高灵活性;(2)与动态编译结合Class.forName(‘com.mysql.jdbc.Driver.class’);//加载MySQL的驱动类

缺点:使用反射性能较低,需要解析字节码,将内存中的对象进行解析。

其解决方案是:通过setAccessible(true)关闭JDK的安全检查来提升反射速度;多次创建一个类的实例时,有缓存会快很多;ReflflectASM工具类,通过字节码生成的方式加快反射速度。

select,poll,epoll的区别

(1) select==>时间复杂度O(n)

它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

缺点

​ (1)单进程可以打开fd有限制;

​ (2)对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低;

​ (2)用户空间和内核空间的复制非常消耗资源;

(2) poll==>时间复杂度O(n)

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

其和select不同的地方:采用链表的方式替换原有fd_set数据结构,而使其没有连接数的限制

(3) epoll==>时间复杂度O(1)

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

对象头

  • 字宽(Word): 内存大小的单位概念, 对于 32 位处理器 1 Word = 4 Bytes, 64 位处理器 1 Word = 8 Bytes

  • 每一个 Java 对象都至少占用 2 个字宽的内存(数组类型占用3个字节的字宽)。

  • 第一个字宽也被称为对象头Mark Word。 对象头包含了多种不同的信息, 其中就包含对象锁相关的信息。

  • 第二个字宽是指向定义该对象类信息(class metadata)的指针

多线程

线程与进程的状态

线程状态:

image-20211012114335901

进程状态:

image-20211012114423733

并发、并行

并发:同一个时间段,多个任务都在执行(交替执行)。

并行:单位时间内,多个任务同时执行。

什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

守护线程和用户线程有什么区别呢?

守护线程和用户线程

  • 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
  • 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作

main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。

比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。

注意事项:

  1. setDaemon(true)必须在start()方法前执行,否则会抛出 IllegalThreadStateException 异常
  2. 在守护线程中产生的新线程也是守护线程
  3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
  4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。

创建线程的四种方式

1)继承 Thread 类;

public class MyThread extends Thread {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + " run()方法正在执
行...");
	}

2)实现Runnable接口

public class MyRunnable implements Runnable {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + " run()方法执行
中...");
	}

3)实现Callable接口

public class MyCallable implements Callable {
	@Override
	public Integer call() {
		System.out.println(Thread.currentThread().getName() + " call()方法执行
中...");
		return 1;
	}

4)使用匿名内部类方式

public class CreateRunnable {
	public static void main(String[] args) {
		//创建多线程创建开始
		Thread thread = new Thread(new Runnable() {
			public void run() {
				for (int i = 0; i < 10; i++) {
					System.out.println("i:" + i);
				}
			}
		}
		);
		thread.start();
	}
}

说一下 runnable 和 callable 有什么区别

线程的 run()和 start()有什么区别

  • 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
  • start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start()只能调用一次。
  • start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
  • run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start()方法而不是run()方法。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

什么是 Callable 和 Future?

  • Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
  • Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。

什么是 FutureTask

  • FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

线程的状态

Java 中用到的线程调度算法是什么

有两种调度模型:分时调度模型、抢占调度模型

Java中使用的是抢占调度模型

sleep() 和 wait() 有什么区别

为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里

为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用

Thread 类中的 yield 方法有什么作用

线程的 sleep()方法和 yield()方法有什么区别

如何停止一个正在运行的线程

Java 中 interrupted 和 isInterrupted 方法的区别?

Java 如何实现多线程之间的通讯和协作

如果你提交任务时,线程池队列已满,这时会发生什么

什么是线程同步和线程互斥,有哪几种实现方式?

线程之间如何通信及线程之间如何同步

  • 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以如何来交换信息。一般线程之间的通信机制有两种:共享内存和消息传递。
  • Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

Synchronized

关键字的使用

底层实现原理

可重入的原理

自旋

synchronized 锁升级的原理

线程 B 怎么知道线程 A 修改了变量

  • (1)volatile 修饰变量
  • (2)synchronized 修饰修改变量的方法
  • (3)wait/notify
  • (4)while 轮询

当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的synchronized 方法 B

synchronized 和 Lock 的区别

  • 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
  • synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
  • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁; 而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到

synchronized 和 ReentrantLock 区别

Java 中能创建 volatile 数组吗

synchronized 和 volatile 的区别

线程池

构造的核心参数

corePoolSize  核心线程数量
maximumPoolSize  最大线程数量
keepAliveTime  线程保持时间,N个时间单位
unit  时间单位(分、秒)
workQueue 阻塞队列
threadFactory   线程工厂
handler	 线程池拒绝策略

Executors框架实现的线程池/线程池的四种创建方式

newCachedThreadPool
newFixedThreadPool
newScheduledThreadPool
newSingleThreadPool

实现的区别就是ThreadPoolExecutor中传入的构造参数不同

四种线程池的特点

线程池的状态

饱和策略(handler)

执行原理

集合

List、Map、Set关系

fail-fast

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件

ArrayList扩容机制

1.首先空构造初始化,指向一个空数组

2.add方法(扩容的触发是添加)

第一次调用,会与默认值(10)比较,看谁大选谁

然后进行判断是否需要扩容

进入扩容函数

扩容后的数量是扩容前的1.5倍

总结:

1.构造方法:初始化一个空数组

2.add方法:1.扩容逻辑 2.赋值 3.返回true

扩容逻辑:1)第一次直接初始化长度为10的数组

2)后续按照1.5倍扩容(满足扩容条件)传入的长度(Mincapacity)大于了现有数组长度(elementData.length)。

HashMap的底层实现

JDK1.8之前HashMap底层是数组和链表。HashMap通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n-1)&hash判断当前元素存放的位置(这里的n是指数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。拉链法是指遇到哈希冲突就将冲突的值加到链表中即可。

JDK1.8以后的HashMap在解决哈希冲突有了较大的变化,当链表长度大于8(链表转换为红黑树前会判断,如果当前数据的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树。)时,将链表转化为红黑树,以减少搜索时间

红黑树

  • 红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红 (Red)或黑(Black)。
  • 红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色 注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!。
  • 如果一个结点是红色的,则它的子结点必须是黑色的。
  • 每个结点到叶子结点NIL所经过的黑色结点的个数一样的。确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!
  • 红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。

PUT的具体操作

  1. 判断键值对数组tablei是否为空或为null,否则执行resize()进行扩容;
  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果tablei不为空,转向③;
  3. 判断tablei的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
  4. 判断tablei 是否为treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;
  5. 遍历tablei,判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容

扩容的操作

Hash函数

HashMap长度为2的幂次方

取模运算是%操作中除数是2的幂次方的话,可以把%等价为&(除数-1)

多线程操作导致死循环

循环数组的每个坑,然后在循环坑里的链表。把原来的元素转移到新的数组

E代表循环的元素

e.next=newTable[i],将元素的Next指针指向新数组

JDK1.8采用了尾节点,就避免了循环了。

JDK1.8不只是采用尾插法去解决了循环链表的问题,并且他还引入了高位链和低位链去解决

ConcurrentHashMap线程安全的具体实现/底层具体实现

JDK1.7:分段锁,将数据分为一段一段存储,然后给每一个数据段分配一个锁。当一个线程占用了其中一个数据段,其他数据段仍然可以被其他线程访问。

一个ConcurrentHashMap包含一个Segment数组。Segement的结构和HashMap类似,是一种数组和链表结构,一个Segement包含一个Hashentry数组,每个HashEntry是一个链表结构的元素,每个Segement守护一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segement的锁。

JDK1.8:ConcurrentHashMap取消了Segment分段锁,采用CAS和sychronized来保证并发安全。数据结构跟HashMap1.8的结构相似,数组+链表/红黑树。Java8在链表长度超过一定阈值(8,并数组长度大于64)将链表(寻址时间复杂度O(N))转化为红黑树(寻址时间复杂度为O(log(N)))

Synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍

迭代器 Iterator

Iterator 和 ListIterator 有什么区别?

  • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
  • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
  • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

设计模式

六大原则

  • 开放封闭原则(Open Close Principle)
  • 里氏代换原则(Liskov Substitution Principle)
  • 依赖倒转原则(Dependence In version Principle)
  • 接口隔离原则(Interface Segregation Principle)

  • 迪米特法则(最少知道原则)(Demeter Principle)

  • 单一职责原则(Principle of single responsibility)

单例模式

使用场景

  1. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  2. 应用程序的日志应用,一般都是单例模式实现,只有一个实例去操作才好,否则内容不好追加显示。
  3. 多线程的线程池的设计一般也是采用单例模式,因为线程池要方便对池中的线程进行控制
  4. Windows的(任务管理器)就是很典型的单例模式,他不能打开俩个
  5. windows的(回收站)也是典型的单例应用。在整个系统运行过程中,回收站只维护一个实例。

双重检验锁方式实现单例模式

饿汉式-静态常量(线程安全)

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

饿汉式-静态代码块(线程安全)

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

懒汉式(线程不安全)

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

懒汉式(线程安全,方法上加同步锁)

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

双重校验锁(线程安全,效率高)

public class Singleton {
	private volatile static Singleton singleton;
	private Singleton() {}
	public static Singleton getSingleton() {
		if (singleton == null) {
			synchronized (Singleton.class) {
				if (singleton == null) {
						singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
}

静态内部类实现单例(线程安全、效率高)

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}

工厂模式

它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。实现了创建者和调用者分离,工厂模式分为简单工厂、工厂方法、抽象工厂模式

使用场景

简单工厂模式

工厂方法模式

抽象工厂模式

代理模式

  • 通过代理控制对象的访问,可以在这个对象调用方法之前、调用方法之后去处理/添加新的功能。(也就是AO的P微实现)
  • 代理在原有代码乃至原业务流程都不修改的情况下,直接在业务流程中切入新代码,增加新功能,这也和Spring的(面向切面编程)很相似

使用场景

Spring AOP、日志打印、异常处理、事务控制、权限控制等

静态代理

由程序员创建或工具生成代理类的源码,再编译代理类。所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了

动态代理

使用反射完成代理。需要有顶层接口才能使用,常见是mybatis的mapper文件是代理

Cglib代理

也是使用反射完成代理,可以直接代理类(jdk动态代理不行),使用字节码技术,不能对 final类进行继承。(需要导入jar包)

观察者模式

观察者模式主要用于1对N的通知。当一个对象的状态变化时,他需要及时告知一系列对象,令他们做出相应。

实现有两种方式:

  1. 推:每次都会把通知以广播的方式发送给所有观察者,所有的观察者只能被动接收。
  2. 拉:观察者只要知道有情况即可,至于什么时候获取内容,获取什么内容,都可以自主决定。

观察者模式应用场景

  1. 关联行为场景,需要注意的是,关联行为是可拆分的,而不是“组合”关系。事件多级触发场景。
  2. 跨系统的消息交换场景,如消息队列、事件总线的处理机制

中间件

redis

Redis也扛不住了,万级流量会打到DB上,该怎么处理

Redis有哪5种数据类型及使用场景

String(简单操作)

  1. 因为Redis的String是二进制安全的,因此可以存储对象。对象存储形式可以为JSON或者序列化后的对象。这边再介绍一种方法,通过MSET方法批量插入对象属性,好处是避免序列化或解析JSON,直接从redis中获取对象相应属性值。

  2. SETNX\DEL可以实现分布式锁。SETNX(SET IF NOT EXISTS):只在键key不存在的情况下,将value设为true,如果key已经存在,则不进行任何操作。因此比如有3个线程希望获得分布式锁,只有返回值为1的线程表示拿到了分布式锁,通过DEL释放锁。问题是,如果拿到锁的线程宕机,那么将不会执行DEL,导致其他线程永远无法拿到锁,解决方案是加一个过期时间。

  3. INCR/DECR 通过计数器实现阅读/点赞/商品的计数

  4. 实现分布式session

  5. INCRBY,批量增加,实现分布式系统全局序列号。比如有10个请求需要对数据库的同一张表进行操作,那么可以用redis给每个请求的进程用INCRBY增加100,相当于第一个进程插入1到100序号,第二个进程插入101到200。因为redis是单线程的,因此用INCRBY这个原子操作让每个请求都分配到了不冲突的号。

hash(结构化数据)

  1. 对象缓存,类似于MSET

  2. 电商购物车 1.以用户id为key 2.以商品id为field 3.商品数量为value。
    标准:hset key field value 举例:hset cart:1001 10099 1,在cart:1001这个key中,有field叫商品id,value为商品id对应的数量,所以举例中的意思是,在cart:1001购物车中,有商品id为10099的商品1个。

用hincrby/hdecrby来增加和删除购物车中商品数量,用hlen来获得购物车总数量,用hgetall来实现全选。

list(有序)

  1. 实现栈、队列、阻塞队列的数据结构。

  2. 实现微博信息流,某大V有100万个关注,那么它的消息就要定时推送给这100万个粉丝,通过LPUSH将消息放到信息流最前,如果你关注的另一个大V随后也发了一个信息,那么他也LPUSH,这样缓存中就是先大V2号的消息,再大V1号的消息,通过LRANGE方法读取即可

set(并交集、是否存在)

  1. 微信抽奖小程序

  2. 微信/微博点赞、收藏、标签

  3. 集合操作实现微博微信关注模型

zset (排序)

  1. 微博热搜

Redis是怎么删除过期key的

lru

Redis有哪几种数据淘汰策略

在Redis中,允许用户设置最大使用内存大小server.maxmemory,当Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

1.volatile-lru:从已设置过期的数据集中挑选最近最少使用的淘汰

2.volatile-ttr:从已设置过期的数据集中挑选将要过期的数据淘汰

3.volatile-random:从已设置过期的数据集中任意挑选数据淘汰

4.allkeys-lru:从数据集中挑选最近最少使用的数据淘汰

5.allkeys-random:从数据集中任意挑选数据淘汰

6.noenviction:禁止淘汰数据

redis淘汰数据时还会同步到aof

Redis有哪些持久化方式

rdb、aof

项目中有用到吗?购物车用的什么数据类型? 为什么要用呢?

hash

redis宕机缓存一致性?

不一致的原因

我们在是使用redis过程中,通常会这样做,先读取缓存,如果缓存不存在,则读取数据库。

不管是先写库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。

因为写和读是并发的,没法保证顺序,如果删除了缓存,还没有来得及写库,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

如果是redis集群,或者主从模式,写主读从,由于redis复制存在一定的时间延迟,也有可能导致数据不一致。

优化思路

(1)读操作优先读取redis,不存在的话就去访问MySql,并把读到的数据写回Redis中;

(2)写操作的话,直接写MySql,成功后再写入Redis,替换掉原来的旧数据(可以在MySql端定义CRUD触发器,在触发CRUD操作后写数据到Redis,也可以在Redis端解析binlog,再做相应的操作)

(3)设定合理的超时时间,即经过超时时间,自动将redis中相应的数据删除。这样最差的情况是在超时时间内,内存存在不一致。当然这种策略要考虑redis和数据库主从同步的耗时,所以在第二次删除前最好休眠一定的时间,比如500毫秒,这样无疑又增加了写请求的耗时。

怎么判断某个节点的主机是否可以正常工作

通过心跳检测机制。
首先要说的是,每一个节点都存有这个集群所有主节点以及从节点的信息。它们之间通过互相的ping-pong判断是否节点可以连接上(心跳检测机制)。如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用节点

一致性哈希算法

https://www.zsythink.net/archives/1182/

解决的问题:

为了在节点数目发生改变时尽可能少的迁移数据,将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash 后会顺时针找到临接的存储节点存放。而当有节点加入或退 时,仅影响该节点在Hash环上顺时针相邻的后续节点。

优点:

加入和删除节点只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响。

缺点:

数据的分布和节点的位置有关,因为这些节点不是均匀的分布在哈希环上的,所以数据在进行存储时达不到均匀分布的效果。

原理:

一致性哈希算法是对2^32取模

hash(服务器A的IP地址) % 2^32

hash(服务器B的IP地址) % 2^32

hash(服务器C的IP地址) % 2^32

hash(图片名称) % 2^32

一致性哈希算法就是首先,判断一个对象应该被缓存到哪台服务器上的(对2^ 32取余),将缓存服务器与被缓存对象都映射到hash环上以后,从被缓存对象的位置出发,沿顺时针方向遇到的第一个服务器,就是当前对象将要缓存于的服务器,由于被缓存对象与服务器hash后的值是固定的,所以,在服务器不变的情况下,一张图片必定会被缓存到固定的服务器上,那么,当下次想要访问这张图片时,只要再次使用相同的算法进行计算,即可算出这个图片被缓存在哪个服务器上,直接去对应的服务器查找对应的图片即可。

哈希槽个数为什么是16384个

一个心跳检测字节为char类型占2kB,

2 * 8(bit) * 1024(1K) = 2^14 = 16384

redis主从复制

Redis主从复制可以根据是否是全量分为全量同步和增量同步。

全量复制

Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
  1)Slave 连接Master ,发送SYNC命令;
  2)Master 接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件 , 同时将这之后新的写命令记入缓冲区;
  3)Master BGSAVE执行完后,向所有Slave 发送快照文件, 并继续记录写命令;
  4)Slave 收到快照文件后丢弃所有旧数据,载入收到的快照;
  5)Master 快照发送完毕后开始向Slave 发送缓冲区中的写命令;
  6)Slave 完成对快照的载入,开始接收命令请求,并执行来自Master 缓冲区的写命令;
  完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。

增量复制

增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

Redis主从同步策略

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步

Redis 适合场景

1)Session共享(单点登录) string

2)页面缓存 hash

3)队列 list

4)排行榜/计数器 zset、 string

5)发布/订阅 list

缓存击穿

缓存中没有,数据库中有,key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方式

  1. 使用互斥锁**(mutex key)**

  2. 设置热点数据永远不过期

  3. 接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制。

缓存雪崩

缓存雪崩是指设置缓存时采用了相同的过期时间,导致缓存在某一个时刻同时失效,或者缓存服务器宕机宕机导致缓存全面失效,请求全部转发到了DB层面,DB由于瞬间压力增大而导致崩溃。缓存失效导致的雪崩效应对底层系统的冲击是很大的。

解决方式

  1. 对缓存的访问,如果发现从缓存中取不到值,那么通过加锁或者队列的方式保证缓存的单进程操作,从而避免失效时并发请求全部落到底层的存储系统上;但是这种方式会带来性能上的损耗
  2. 将缓存失效的时间分散,降低每一个缓存过期时间的重复率
  3. 如果是因为缓存服务器故障导致的问题,一方面需要保证缓存服务器的高可用、另一方面,应用程序中可以采用多级缓存

缓存穿透

缓存穿透是指查询一个根本不存在的数据,缓存和数据源都不会命中。出于容错的考虑,如果从数据层查不到数据则不写入缓存,即数据源返回值为 null 时,不缓存 null。缓存穿透问题可能会使后端数据源负载加大,由于很多后端数据源不具备高并发性,甚至可能造成后端数据源宕掉

解决方式

  1. 如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。比如,”key” , “&&”。

在返回这个&&值的时候,我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待继续访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是&&,则可以认为这时候key有值了,从而避免了透传到数据库,从而把大量的类似请求挡在了缓存之中。

  1. 根据缓存数据Key的设计规则,将不符合规则的key进行过滤

采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的BitSet中,不存在的数据将会被拦截掉,从而避免了对底层存储系统的查询压力

布隆过滤器

布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器是一种空间效率极高的概率型算法和数据结构,主要用来判断一个元素是否在集合中存在。因为他是一个概率型的算法,所以会存在一定的误差,如果传入一个值去布隆过滤器中检索,可能会出现检测存在的结果但是实际上可能是不存在的,但是肯定不会出现实际上不存在然后反馈存在的结果。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省

  • bitmap
    所谓的Bit-map就是用一个bit位来标记某个元素对应的Value,通过Bit为单位来存储数据,可以大大节省存储空间.所以我们可以通过一个int型的整数的32比特位来存储32个10进制的数字,那么这样所带来的好处是内存占用少、效率很高(不需要比较和位移)比如我们要存储5(101)、3(11)四个数字,那么我们申请int型的内存空间,会有32个比特位。这四个数字的二进制分别对应从右往左开始数,比如第一个数字是5,对应的二进制数据是101, 那么从右往左数到第5位,把对应的二进制数据存储到32个比特位上。
    第一个5就是 00000000000000000000000000101000
    输入3时候 00000000000000000000000000001100
  • 布隆过滤器原理

有了对位图的理解以后,我们对布隆过滤器的原理理解就会更容易了,仍然以前面提到的40亿数据为案例,假设这40亿数据为某邮件服务器的黑名单数据,邮件服务需要根据邮箱地址来判断当前邮箱是否属于垃圾邮件。原理如下

假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中处。

分布式锁

kafka

为什么要使用

缓冲和削峰:上游数据时有突发流量,下游可能扛不住,或者下游没有足够多的机器来保证冗余,kafka在中间可以起到一个缓冲的作用,把消息暂存在kafka中,下游服务就可以按照自己的节奏进行慢慢处理。

解耦和扩展性:项目开始的时候,并不能确定具体需求。消息队列可以作为一个接口层,解耦重要的业务流程。只需要遵守约定,针对数据编程即可获取扩展能力。

冗余:可以采用一对多的方式,一个生产者发布消息,可以被多个订阅topic的服务消费到,供多个毫无关联的业务使用。

健壮性:消息队列可以堆积请求,所以消费端业务即使短时间死掉,也不会影响主要业务的正常进行。

异步通信:很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。

broker作用

broker 是消息的代理,Producers往Brokers里面的指定Topic中写消息,Consumers从Brokers里面拉取指定Topic的消息,然后进行业务处理,broker在中间起到一个代理保存消息的中转站。

kafka中的 zookeeper 起到什么作用

zookeeper 是一个分布式的协调组件,早期版本的kafka用zk做meta信息存储,consumer的消费状态,group的管理以及 offset的值。考虑到zk本身的一些因素以及整个架构较大概率存在单点问题,新版本中逐渐弱化了zookeeper的作用。新的consumer使用了kafka内部的group coordination协议,也减少了对zookeeper的依赖,

但是broker依然依赖于ZK,zookeeper 在kafka中还用来选举controller 和 检测broker是否存活等等。

follower如何与leader同步数据

Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。完全同步复制要求All Alive Follower都复制完,这条消息才会被认为commit,这种复制方式极大的影响了吞吐率。而异步复制方式下,Follower异步的从Leader复制数据,数据只要被Leader写入log就被认为已经commit,这种情况下,如果leader挂掉,会丢失数据,kafka使用ISR的方式很好的均衡了确保数据不丢失以及吞吐率。Follower可以批量的从Leader复制数据,而且Leader充分利用磁盘顺序读以及send file(zero copy)机制,这样极大的提高复制性能,内部批量写磁盘,大幅减少了Follower与Leader的消息量差。

producer如何优化打入速度

  • 增加线程
  • 提高 batch.size
  • 增加更多 producer 实例
  • 增加 partition 数
  • 设置 acks=-1 时,如果延迟增大:可以增大 num.replica.fetchers(follower 同步数据的线程数)来调解;
  • 跨数据中心的传输:增加 socket 缓冲区设置以及 OS tcp 缓冲区设置

Kafka中的消息是否会丢失和重复消费

要确定Kafka的消息是否丢失或重复,从两个方面分析入手:消息发送和消息消费。

1、消息发送

     Kafka消息发送有两种方式:同步(sync)和异步(async),默认是同步方式,可通过producer.type属性进行配置。Kafka通过配置request.required.acks属性来确认消息的生产:

0—表示不进行消息接收是否成功的确认;
1—表示当Leader接收成功时确认;
-1—表示Leader和Follower都接收成功时确认;
综上所述,有6种消息生产的情况,下面分情况来分析消息丢失的场景:

(1)acks=0,不和Kafka集群进行消息接收确认,则当网络异常、缓冲区满了等情况时,消息可能丢失;

(2)acks=1、同步模式下,只有Leader确认接收成功后但挂掉了,副本没有同步,数据可能丢失;

2、消息消费

Kafka消息消费有两个consumer接口,Low-level API和High-level API:

Low-level API:消费者自己维护offset等值,可以实现对Kafka的完全控制;

High-level API:封装了对parition和offset的管理,使用简单;

如果使用高级接口High-level API,可能存在一个问题就是当消息消费者从集群中把消息取出来、并提交了新的消息offset值后,还没来得及消费就挂掉了,那么下次再消费时之前没消费成功的消息就“诡异”的消失了;

解决办法:

    针对消息丢失:同步模式下,确认机制设置为-1,即让消息写入Leader和Follower之后再确认消息发送成功;异步模式下,为防止缓冲区满,可以在配置文件设置不限制阻塞超时时间,当缓冲区满时让生产者一直处于阻塞状态;

    针对消息重复:将消息的唯一标识保存到外部介质中,每次消费时判断是否处理过即可。

kafka如何实现延迟队列

Kafka并没有使用JDK自带的Timer或者DelayQueue来实现延迟的功能,而是基于时间轮自定义了一个用于实现延迟功能的定时器(SystemTimer)。JDK的Timer和DelayQueue插入和删除操作的平均时间复杂度为O(nlog(n)),并不能满足Kafka的高性能要求,而基于时间轮可以将插入和删除操作的时间复杂度都降为O(1)。时间轮的应用并非Kafka独有,其应用场景还有很多,在Netty、Akka、Quartz、Zookeeper等组件中都存在时间轮的踪影。

底层使用数组实现,数组中的每个元素可以存放一个TimerTaskList对象。TimerTaskList是一个环形双向链表,在其中的链表项TimerTaskEntry中封装了真正的定时任务TimerTask.

Kafka中到底是怎么推进时间的呢?Kafka中的定时器借助了JDK中的DelayQueue来协助推进时间轮。具体做法是对于每个使用到的TimerTaskList都会加入到DelayQueue中。Kafka中的TimingWheel专门用来执行插入和删除TimerTaskEntry的操作,而DelayQueue专门负责时间推进的任务。再试想一下,DelayQueue中的第一个超时任务列表的expiration为200ms,第二个超时任务为840ms,这里获取DelayQueue的队头只需要O(1)的时间复杂度。如果采用每秒定时推进,那么获取到第一个超时的任务列表时执行的200次推进中有199次属于“空推进”,而获取到第二个超时任务时有需要执行639次“空推进”,这样会无故空耗机器的性能资源,这里采用DelayQueue来辅助以少量空间换时间,从而做到了“精准推进”。Kafka中的定时器真可谓是“知人善用”,用TimingWheel做最擅长的任务添加和删除操作,而用DelayQueue做最擅长的时间推进工作,相辅相成。

ES

项目中为什么用到

底层原理 (倒排索引,存储结构,压缩技巧)

MongoDB

分片(sharding)和复制(replication)是怎样工作的

每一个分片(shard)是一个分区数据的逻辑集合。分片可能由单一服务器或者集群组成,我们推荐为每一

个分片(shard)使用集群

数据在什么时候才会扩展到多个分片(shard)里

MongoDB 分片是基于区域(range)的。所以一个集合(collection)中的所有的对象都被存放到一个块

(chunk)中。只有当存在多余一个块的时候,才会有多个分片获取数据的选项。现在,每个默认块的大小

是 64Mb,所以你需要至少 64 Mb 空间才可以实施一个迁移。

当我试图更新一个正在被迁移的块(chunk)上的文档时会发生什么

更新操作会立即发生在旧的分片(shard)上,然后更改才会在所有权转移(ownership transfers)前复制到新的分片上。

如果在一个分片(shard)停止或者很慢的时候,我发起一个查询会怎样

如果一个分片(shard)停止了,除非查询设置了“Partial(局部)”选项,否则查询会返回一个错误。如果一个分片(shard)响应很慢,MongoDB 则会等待它的响应。

我可以把 moveChunk 目录里的旧文件删除吗

没问题,这些文件是在分片(shard)进行均衡操作(balancing)的时候产生的临时文件。一旦这些操作已经完成,相关的临时文件也应该被删除掉。但目前清理工作是需要手动的,所以请小心地考虑再释放这些文件的空间。

我怎么查看 Mongo 正在使用的链接

db._adminCommand(“connPoolStats”);

如果块移动操作(moveChunk)失败了,我需要手动清除部分转移的文档吗

不需要,移动操作是一致(consistent)并且是确定性的(deterministic);一次失败后,移动操作会不断重试; 当完成后,数据只会出现在新的分片里(shard)。

如果我在使用复制技术(replication),可以一部分使用日志(journaling)而其他部分则不使用吗

可以

当更新一个正在被迁移的块(Chunk)上的文档时会发生什么

更新操作会立即发生在旧的块(Chunk)上,然后更改才会在所有权转移前复制到新的分片上。

MongoDB 在 A:{B,C}上建立索引,查询 A:{B,C}和 A:{C,B}都会使用索引吗

不会,只会在 A:{B,C}上使用索引。

MongoDB 支持存储过程吗?如果支持的话,怎么用

MongoDB 支持存储过程,它是 javascript 写的,保存在 db.system.js 表中。

如何理解 MongoDB 中的 GridFS 机制,MongoDB 为何使用 GridFS 来存储文件

GridFS 是一种将大型文件存储在 MongoDB 中的文件规范。使用 GridFS 可以将大文件分隔成多个小文档存放,这样我们能够有效的保存大文档,而且解决了 BSON 对象有限制的问题。

Linux

linux如何查看端口是否被使用

  1. netstat -anp | grep 端口号

  2. netstat -nultp(此处不用加端口号)

该命令是查看当前所有已经使用的端口情况

Linux复杂的查找命令

一.find命令

**基本格式:**find path expression

1.按照文件名查找

(1)find / -name httpd.conf  #在根目录下查找文件httpd.conf,表示在整个硬盘查找
    (2)find /etc -name httpd.conf  #在/etc目录下文件httpd.conf
    (3)find /etc -name ‘srm’  #使用通配符*(0或者任意多个)。表示在/etc目录下查找文件名中含有字符串‘srm’的文件
    (4)find . -name ‘srm*’   #表示当前目录下查找文件名开头是字符串‘srm’的文件

2.按照文件特征查找

(1)find / -amin -10   # 查找在系统中最后10分钟访问的文件(access time)
    (2)find / -atime -2   # 查找在系统中最后48小时访问的文件
    (3)find / -empty   # 查找在系统中为空的文件或者文件夹
    (4)find / -group cat   # 查找在系统中属于 group为cat的文件
    (5)find / -mmin -5   # 查找在系统中最后5分钟里修改过的文件(modify time)
    (6)find / -mtime -1   #查找在系统中最后24小时里修改过的文件
    (7)find / -user fred   #查找在系统中属于fred这个用户的文件
    (8)find / -size +10000c  #查找出大于10000000字节的文件(c:字节,w:双字,k:KB,M:MB,G:GB)
    (9)find / -size -1000k   #查找出小于1000KB的文件

3.使用混合查找方式查找文件

参数有: !,-and(-a),-or(-o)。

(1)find /tmp -size +10000c -and -mtime +2   #在/tmp目录下查找大于10000字节并在最后2分钟内修改的文件
   (2)find / -user fred -or -user george   #在/目录下查找用户是fred或者george的文件文件
   (3)find /tmp ! -user panda  #在/tmp目录中查找所有不属于panda用户的文件

二、grep命令

***基本格式:*find expression

1.主要参数

[options]主要参数:
    -c:只输出匹配行的计数。
    -i:不区分大小写
    -h:查询多文件时不显示文件名。
    -l:查询多文件时只输出包含匹配字符的文件名。
    -n:显示匹配行及行号。
    -s:不显示不存在或无匹配文本的错误信息。
    -v:显示不包含匹配文本的所有行。

pattern正则表达式主要参数:
    \: 忽略正则表达式中特殊字符的原有含义。
    ^:匹配正则表达式的开始行。
    $: 匹配正则表达式的结束行。
    <:从匹配正则表达 式的行开始。
    >:到匹配正则表达式的行结束。
    [ ]:单个字符,如[A]即A符合要求 。
    [ - ]:范围,如[A-Z],即A、B、C一直到Z都符合要求 。
    .:所有的单个字符。
    * :有字符,长度可以为0。

2.实例

(1)grep ‘test’ d*  #显示所有以d开头的文件中包含 test的行
  (2)grep ‘test’ aa bb cc    #显示在aa,bb,cc文件中包含test的行
  (3)grep ‘[a-z]{5}’ aa   #显示所有包含每行字符串至少有5个连续小写字符的字符串的行
  (4)grep magic /usr/src  #显示/usr/src目录下的**文件(不含子目录)包含magic的行
  (5)grep -r magic /usr/src  #显示/usr/src目录下的
文件(包含子目录)**包含magic的行

(6)grep -w pattern files :只匹配整个单词,而不是字符串的一部分(如匹配’magic’,而不是’magical’),

追加重定向和清空重定向的区别(> , >>)

> 是定向输出到文件,如果文件不存在,就创建文件;如果文件存在,就将其清空;一般我们备份清理日志文件的时候,就是这种方法:先备份日志,再用>,将日志文件清空(文件大小变成0字节); >>是将输出内容追加到目标文件中。如果文件不存在,就创建文件;如果文件存在,则将新的内容追加到那个文件的末尾,该文件中的原有内容不受影响。

Spring

Spring中重要的模块

Spring Core可以说Spring其他所有的功能都需要依赖该类库。主要提供IoC依赖注入功能

Spring AOP: 提供面向切面的编程实现。

Spring JDBC: Java数据库连接。

Spring ORM: 用于支持Hibernate等ORM工具。

Spring Web:为创建Web应用程序提供支持。

Spring Test:提供了对Junit测试的支持。

IOC

IoC(控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。IoC在其他语言中也有应用,并非Spring特有。IoC容器是Spring用来实现IoC的载体,IoC容器实际上就是个Map(key,value),Map中存放的是各种对象

将对象之间的相互依赖关系交给IoC容器来管理,并由IoC容器完成出来对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IoC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能有几百甚至上千类作为它的底层,加入我们需要实例化这个Service,你可能要每次都要搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用了IoC的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度

直接去工厂取,而不是去关联对象,因为可能一个类中会用多个对象,有工厂的话,那么只需要工厂一个类,而不是很多的类,减少耦合。

AOP

AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务管理、日志管理、权限控制)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Spring AOP是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象(有些类单类,不去实现接口),就无法使用JDK Proxy去进行代理,这时候Spring AOP会使用Cglib,这时候Spring AOP会使用Cglib生成一个被代理对象的子类来作为代理,如下图所示:

Bean的作用域

后三个仅在WEB框架使用。

  • Singleton:唯一bean实例,Spring中的bean是单例;
  • Prototype:每次请求都会创建一个新的bean实例,一个bean对象多个对象实例
  • Request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效
  • Session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效
  • Global-session:全局session作用域,

spring 中Bean的生命周期

Spring框架中用到的设计模式

工厂设计模式:Spring使用工厂模式通过BeanFactory、ApplicationContext创建bean对象。

代理模式:Spring AOP功能的实现。

单例设计模式:Spring中的Bean默认都是单例的。

包装器设计模式:我们的项目需要连接不同类型的数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们根据客户的需求能够动态切换不同的数据源。

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

适配器模式:SpringAOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是使用了适配器模式适配Controller。 加一个适配器的类,引入网线,转成USB,这样电脑用USB就可以上网了

Spring MVC的运行流程

(1)用户发送请求至前端控制器DispatcherServlet;

(2) DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handle;

(3)处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;

(4)DispatcherServlet 调用 HandlerAdapter处理器适配器;

(5)HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控制器);

(6)Handler执行完成返回ModelAndView;

(7)HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;

(8)DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;

(9)ViewResolver解析后返回具体View;

(10)DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)

(11)DispatcherServlet响应用户。

Spring MVC的主要组件

(1)前端控制器 DispatcherServlet(不需要程序员开发)

作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。

Spring的MVC框架是围绕DispatcherServlet来设计的,它用来处理所有的HTTP请求和响应。

(2)处理器映射器HandlerMapping(不需要程序员开发)

作用:根据请求的URL来查找Handler

(3)处理器适配器HandlerAdapter

注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。

(4)处理器Handler(需要程序员开发)

(5)视图解析器 ViewResolver(不需要程序员开发)

作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view)

(6)视图View(需要程序员开发jsp)

View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等)

SpringBoot的特征?

  • 创建独立的Spring应用程序
  • 直接嵌入Tomcat,Jetty或Undertow(无需部署WAR文件)
  • 提供固化的“starter”依赖项,以简化构建配置
  • 尽可能自动配置Spring和3rd Party库
  • 提供可用于生产的功能,例如指标,运行状况检查和外部化配置
  • 完全没有代码生成,也不需要XML配置

Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

  • @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。

  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项, 例 如: java 如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。

  • @ComponentScan:Spring组件扫描。

Spring Boot 配置加载顺序?

Spring Boot 实现全局异常处理

  • Spring 提供了一种使用 ControllerAdvice 处理异常的非常有用的方法。 我们通过实现一个ControlerAdvice 类,来处理控制器类抛出的所有异常。

Spring Boot 中的监视器

Spring Boot 中如何解决跨域问题

跨域可以在前端通过 JSONP 来解决,但是 JSONP 只可以发送 GET 请求,无法发送其他类型的请求,在 RESTful 风格的应用中,就显得非常鸡肋,因此我们推荐在后端通过 (CORS,Crossorigin resource sharing) 来解决跨域问题。这种解决方案并非 Spring Boot 特有的,在传统的SSM 框架中,就可以通过 CORS 来解决跨域问题,只不过之前我们是在 XML 文件中配置 CORS ,现在可以通过实现WebMvcConfifigurer接口然后重写addCorsMappings方法解决跨域问题

@Configuration
public class CorsConfig implements WebMvcConfigurer {
	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
		.allowedOrigins("*")
		.allowCredentials(true)
		.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
		.maxAge(3600);
	}
}

SpringBoot读取配置相关注解

  • @PropertySource
  • @Value
  • @Environment
  • @ConfifigurationProperties

SpringBoot异常处理相关注解

  • @ControllerAdvice
  • @ExceptionHandler

Spring Boot 中如何实现定时任务 ?

  • 在 Spring Boot 中使用定时任务主要有两种不同的方式,一个就是使用 Spring 中的 @Scheduled注解,另一-个则是使用第三方框架 Quartz。

Spring Boot 中的 starter 到底是什么 ?

你可能感兴趣的:(java,redis,数据库)