# javaLearn **Repository Path**: gdhyzl/learn ## Basic Information - **Project Name**: javaLearn - **Description**: 自己总结的java经验 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: dev - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2025-12-01 - **Last Updated**: 2025-12-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README *越努力越幸运** Java 1. **[JavaGuide](https://github.com/Snailclimb/JavaGuide)** :【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。(基础,覆盖全) 2. **[advanced-java](https://github.com/doocs/advanced-java)** :互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识。(侧重分布式) 3. **[miaosha](https://github.com/qiurunze123/miaosha)** : 秒杀系统设计与实现.互联网工程师进阶与分析。(侧重项目实战) 4. **[architect-awesome](https://github.com/xingshaocheng/architect-awesome)** :后端架构师技术图谱。 5. **[toBeTopJavaer](https://github.com/hollischuang/toBeTopJavaer)** :Java 工程师成神之路 。 6. **[technology-talk](https://github.com/aalansehaiyang/technology-talk)** : 汇总 java 生态圈常用技术框架、开源中间件,系统架构、数据库、大公司架构案例、常用三方类库、项目管理、线上问题排查、个人成长、思考等知识 7. **[tutorials](https://github.com/eugenp/tutorials)**:该项目是一系列小而专注的教程 - 每个教程都涵盖 Java 生态系统中单一且定义明确的开发领域。 当然,它们的重点是 Spring Framework - Spring,Spring Boot 和 Spring Securiyt。 除了 Spring 之外,还有以下技术:核心 Java,Jackson,HttpClient,Guava。 8. **[JCSprout](https://github.com/crossoverJie/JCSprout)** :处于萌芽阶段的 Java 核心知识库。 9. **[fullstack-tutorial](https://github.com/frank-lam/fullstack-tutorial)** :后台技术栈/架构师之路/全栈开发社区,春招/秋招/校招/面试。 10. **[JavaFamily](https://github.com/AobingJava/JavaFamily)** :【互联网一线大厂面试+学习指南】进阶知识完全扫盲。 11. **[JGrowing](https://github.com/javagrowing/JGrowing)** :Java is Growing up but not only Java。Java 成长路线,但学到不仅仅是 Java。 12. **[interview_internal_reference](https://github.com/0voice/interview_internal_reference)** :2019 年最新总结,阿里,腾讯,百度,美团,头条等技术面试题目,以及答案,专家出题人分析汇总。 13. **[effective-java-3rd-chinese](https://github.com/sjsdfg/effective-java-3rd-chinese)**:Effective Java 中文版(第 3 版),Java 四大名著之一,本书一共包含 90 个条目,每个条目讨论 Java 程序设计中的一条规则。这些规则反映了最有经验的优秀程序员在实践中常用的一些有益的做法。 14. **[OnJava8](https://github.com/LingCoder/OnJava8)**:《On Java 8》中文版,又名《Java 编程思想》第 5 版, Java 四大名著之一。 15. **[java-design-patterns](https://github.com/iluwatar/java-design-patterns)** : Design patterns implemented in Java。 ### 数据结构/算法 1. **[LeetCodeAnimation](https://github.com/MisterBooo/LeetCodeAnimation)** :Demonstrate all the questions on LeetCode in the form of animation.(用动画的形式呈现解 LeetCode 题目的思路)。 2. **[TheAlgorithms-Java](https://github.com/TheAlgorithms/Java)** :All Algorithms implemented in Java。 3. **[leetcode](https://github.com/doocs/leetcode)** :多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解。 4. **[LeetCode-Solution-in-Good-Style](https://github.com/liweiwei1419/LeetCode-Solution-in-Good-Style)** :这个项目是作者在学习《算法与数据结构》的时候,在 [LeetCode(力扣)](https://leetcode-cn.com/) 上做的练习,刷题以 Java 语言为主。作者在刷题的时候,非常考虑代码质量,他的很多问题的回答都被 Leetcode 官方精选,值得推荐! 5. **[Algorithms-in-4-Steps](https://github.com/Xunzhuo/Algorithms-in-4-Steps)** :四步从0到1系统学习算法和数据结构。 ### 计算机基础 1. **[CS-Notes](https://github.com/CyC2018/CS-Notes)** :技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。 2. **[Waking-Up](https://github.com/wolverinn/Waking-Up)** :计算机基础(计算机网络/操作系统/数据库/Git...)面试问题全面总结,包含详细的 follow-up question 以及答案;全部采用【问题+追问+答案】的形式,即拿即用,直击互联网大厂面试 🚀;可用于模拟面试、面试前复习、短期内快速备战面试... ### SpringBoot 1. **[springboot-guide](https://github.com/Snailclimb/springboot-guide)** :SpringBoot 核心知识点总结。 基于 Spring Boot 2.19+。 2. **[SpringAll](https://github.com/wuyouzhuguli/SpringAll)** :循序渐进,学习 Spring Boot、Spring Boot & Shiro、Spring Cloud、Spring Security & Spring Security OAuth2,博客 Spring 系列源码。 3. **[springboot-learning-example](https://github.com/JeffLi1993/springboot-learning-example)** :Spring Boot 实践学习案例,是 Spring Boot 初学者及核心技术巩固的最佳实践。 4. **[spring-boot-demo](https://github.com/xkcoding/spring-boot-demo)** :spring boot demo 是一个用来深度学习并实战 spring boot 的项目,目前总共包含 63 个集成 demo,已经完成 52 个。 5. **[SpringBoot-Labs](https://github.com/YunaiV/SpringBoot-Labs)** :Spring Boot 系列教程。 相关文章:[Github 点赞接近 100k 的 SpringBoot 学习教程+实战推荐!牛批!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247488298&idx=3&sn=0a8fd88ec5a050de131c2a3305482ac4&chksm=cea25ce1f9d5d5f7f53a0237d27489326bce4546353b038085c03b086d91ef396bf824d3a155&token=496868067&lang=zh_CN#rd) ### SpringCloud 1. **[SpringCloudLearning](https://github.com/forezp/SpringCloudLearning)** : 方志朋的《史上最简单的 Spring Cloud 教程源码》。 2. **[SpringCloud-Learning](https://github.com/dyc87112/SpringCloud-Learning)** : Spring Cloud 基础教程,持续连载更新中。 3. **[spring-cloud](https://github.com/yinjihuan/spring-cloud)** : 《Spring Cloud 微服务-全栈技术与案例解析》和《Spring Cloud 微服务 入门 实战与进阶》配套源码。 4. **[spring-cloud-examples](https://github.com/ityouknow/spring-cloud-examples)** :Spring Cloud 学习案例,服务发现、服务治理、链路追踪、服务监控等 (基本没更新了,Spring Cloud 比较老了)。 5. **[SpringCloud](https://github.com/zhoutaoo/SpringCloud)** :基于 SpringCloud2.1 的微服务开发脚手架,整合了 spring-security-oauth2、nacos、feign、sentinel、springcloud-gateway 等。服务治理方面引入 elasticsearch、skywalking、springboot-admin、zipkin 等,让项目开发快速进入业务开发,而不需过多时间花费在架构搭建上。 相关文章:[Github 点赞接近 70k 的 Spring Cloud 学习教程+实战项目推荐!牛批!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247488377&idx=1&sn=0fb33ef330159db5a9c8bc0f029cd739&chksm=cea25cb2f9d5d5a4c7bacc9dcfc90ed86e89f4262e32b40c7aa47af84c747cb6c0429f753e1d&token=496868067&lang=zh_CN#rd) 实战项目 *Guide 哥注:下面这些推荐的项目几乎都和 Spring Boot 有关,毕竟这年头没有理由再搞 SSM/SSH 这些东西了。* 商城系统 *Guide 哥注:下面的商城系统大多比较复杂比如 mall ,如果没有 Java 基础和 Spring Boot 都还没有摸熟的话不推荐过度研究下面几个项目或者使用这些项目当作毕业设计。* 1. **[mall](https://github.com/macrozheng/mall)** :mall 项目是一套电商系统,包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现。 2. **[mall-swarm](https://github.com/macrozheng/mall-swarm)** : mall-swarm 是一套微服务商城系统,采用了 Spring Cloud Greenwich、Spring Boot 2、MyBatis、Docker、Elasticsearch 等核心技术,同时提供了基于 Vue 的管理后台方便快速搭建系统。 3. **[onemall](https://github.com/YunaiV/onemall)** :mall 商城,基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。 4. **[litemall](https://github.com/linlinjava/litemall)** : 又一个小商城。litemall = Spring Boot 后端 + Vue 管理员前端 + 微信小程序用户前端 + Vue 用户移动端。 5. **[xmall](https://github.com/Exrick/xmall)** :基于 SOA 架构的分布式电商购物商城 前后端分离 前台商城:Vue 全家桶 后台管理系统:Spring/Dubbo/SSM/Elasticsearch/Redis/MySQL/ActiveMQ/Shiro/Zookeeper 等 6. **[newbee-mall](https://github.com/newbee-ltd/newbee-mall)** :newbee-mall 项目(新蜂商城)是一套电商系统,包括 newbee-mall 商城系统及 newbee-mall-admin 商城后台管理系统,基于 Spring Boot 2.X 及相关技术栈开发。 ### 博客/论坛/其他 *Guide 哥注:下面这几个项目都是非常适合 Spring Boot 初学者学习的,下面的大部分项目的总体代码架构我都看过,个人觉得还算不错,不会误导没有实际做过项目的老哥,特别是前两个项目 vhr 和 favorites-web 。* 1. **[vhr](https://github.com/lenve/vhr)** :微人事是一个前后端分离的人力资源管理系统,项目采用 SpringBoot+Vue 开发。 2. **[favorites-web](https://github.com/cloudfavorites/favorites-web)** :云收藏 Spring Boot 2.X 开源项目。云收藏是一个使用 Spring Boot 构建的开源网站,可以让用户在线随时随地收藏的一个网站,在网站上分类整理收藏的网站或者文章。 3. **[community](https://github.com/codedrinker/community)** :开源论坛、问答系统,现有功能提问、回复、通知、最新、最热、消除零回复功能。功能持续更新中…… 技术栈 Spring、Spring Boot、MyBatis、MySQL/H2、Bootstrap。 4. **[VBlog](https://github.com/lenve/VBlog)** :V 部落,Vue+SpringBoot 实现的多用户博客管理平台! 5. **[My-Blog](https://github.com/ZHENFENG13/My-Blog)** : My Blog 是由 SpringBoot + Mybatis + Thymeleaf 等技术实现的 Java 博客系统,页面美观、功能齐全、部署简单及完善的代码,一定会给使用者无与伦比的体验。 相关文章: 1. [想要搭建个人博客?我调研了 100 来个 Java 开源博客系统,发现这 5 个最好用!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247491191&idx=1&sn=fd5efa645c2f2e09f088f6a018cea028&chksm=cea251bcf9d5d8aac8653c686b7a331ffe4e13aa9ffc9beab2c378ea2497a9bd3295ff8d2c51&token=747074901&lang=zh_CN#rd) 权限管理系统 *Guide 哥注:权限管理系统在企业级的项目中一般都是非常重要的,如果你需求去实际了解一个不错的权限系统是如何设计的话,推荐你可以参考下面这些开源项目。* 1. **[Spring-Cloud-Admin](https://github.com/wxiaoqi/Spring-Cloud-Admin)** :Cloud-Admin 是国内首个基于 Spring Cloud 微服务化开发平台,具有统一授权、认证后台管理系统,其中包含具备用户管理、资源权限管理、网关 API 管理等多个模块,支持多业务系统并行开发,可以作为后端服务的开发脚手架。代码简洁,架构清晰,适合学习和直接项目中使用。核心技术采用 Spring Boot2 以及 Spring Cloud Gateway 相关核心组件,前端采用 vue-element-admin 组件。 2. **[pig](https://gitee.com/log4j/pig)**:(gitee)基于 Spring Boot 2.2、 Spring Cloud Hoxton & Alibaba、 OAuth2 的 RBAC 权限管理系统。 3. **[FEBS-Shiro](https://github.com/wuyouzhuguli/FEBS-Shiro)** :Spring Boot 2.1.3,Shiro1.4.0 & Layui 2.5.4 权限管理系统。 4. **[eladmin](https://github.com/elunez/eladmin)** : 项目基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、redis、Vue 的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由。 快速开发脚手架 1. **[RuoYi](https://gitee.com/y_project/RuoYi)** :RuoYi 一款基于基于 SpringBoot 的权限管理系统 易读易懂、界面简洁美观,直接运行即可用 。 2. **[Guns](https://gitee.com/stylefeng/guns)** : 我在上大学的时候就了解和接触过了这个项目,当时我还是一个 Spring 入门不太久的小菜鸟。一晃,不经意间已经过去快 3 年了。**Guns 功能齐全 ,采用主流框架 Spring Boot2.0+开发,并且支持 Spring Cloud Alibaba 微服务)。 适合企业后台管理网站的快速开发场景,不论是对于单体和微服务都有支持。** 3. **[SpringBlade](https://gitee.com/smallc/SpringBlade)** :SpringBlade 是一个由商业级项目升级优化而来的 SpringCloud 分布式微服务架构、SpringBoot 单体式微服务架构并存的综合型项目,采用 Java8 API 重构了业务代码,完全遵循阿里巴巴编码规范。采用 Spring Boot 2 、Spring Cloud Hoxton 、Mybatis 等核心技术,同时提供基于 React 和 Vue 的两个前端框架用于快速搭建企业级的 SaaS 多租户微服务平台。 4. **[renren](https://www.renren.io/)** : renren 下面一共开源了两个 Java 项目开发脚手架:①renren-security :采用 Spring、MyBatis、Shiro 框架,开发的一套轻量级权限系统,极低门槛,拿来即用。②renren-fast : 一个轻量级的 Java 快速开发平台,能快速开发项目并交付【接私活利器】 5. **[COLA](https://github.com/alibaba/COLA)** :根据我的了解来看,很多公司的项目都是基于 COLA 进行开发的,相比于其他快速开发脚手架,COLA 并不提供什么已经开发好的功能,它提供的主要是一个干净的架构,然后你可以在此基础上进行开发。通过一行命令就生成好的 web 后端项目骨架。 6. **[generator-jhipster](https://github.com/jhipster/generator-jhipster)** :开源应用程序平台,可在几秒钟内创建 Spring Boot + Angular / React 项目! 7. **[jeecg-boot](https://github.com/zhangdaiscott/jeecg-boot)** :一款基于代码生成器的 JAVA 快速开发平台,开源界“小普元”超越传统商业企业级开发平台! 8. **[zuihou-admin-cloud](https://github.com/zuihou/zuihou-admin-cloud)** :基于`SpringCloud(Hoxton.SR7)` + `SpringBoot(2.2.9.RELEASE)` 的 SaaS 型微服务快速开发平台,具备用户管理、资源权限管理、网关统一鉴权、Xss 防跨站攻击、自动代码生成、多存储系统、分布式事务、分布式定时任务等多个模块,支持多业务系统并行开发, 支持多服务并行开发,可以作为后端服务的开发脚手架。 # 1.java基础 ## **1.1** 集合框架 ![img](http://cdn.processon.com/5e72d92fe4b06b852fe5f78e?e=1584588608&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:qUjLWtKmlUoylliDTLHEldJVyYY=) ### 1.Collection集合总结 Collection |--List 有序,可重复 |--ArrayList 底层数据结构是数组,查询快,增删慢。 线程不安全,效率高 |--Vector 底层数据结构是数组,查询快,增删慢。 线程安全,效率低 |--LinkedList 底层数据结构是链表,查询慢,增删快。 线程不安全,效率高 |--Set 无序,唯一 |--HashSet 底层数据结构是哈希表。 |--LinkedHashSet 底层数据结构是链表和哈希表 由链表保证元素有序 由哈希表保证元素唯一 |--TreeSet 底层数据结构是红黑树。 **如何保证元素唯一性的呢?** 依赖两个方法:hashCode()和equals() 开发中自动生成这两个方法即可 **如何保证元素排序的呢?** 自然排序 比较器排序 如何保证元素唯一性的呢? 根据比较的返回值是否是0来决定 **针对Collection集合我们到底使用谁呢?唯一吗?** 是:Set 排序吗? 是:TreeSet 否:HashSet 如果你知道是Set,但是不知道是哪个Set,就用HashSet。 否:List 要安全吗? 是:Vector 否:ArrayList或者LinkedList 查询多:ArrayList 增删多:LinkedList 如果你知道是List,但是不知道是哪个List,就用ArrayList。 如果你知道是Collection集合,但是不知道使用谁,就用ArrayList。 如果你知道用集合,就用ArrayList 18:在集合中常见的数据结构(掌握) ArrayXxx:底层数据结构是数组,查询快,增删慢 LinkedXxx:底层数据结构是链表,查询慢,增删快 HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals() TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序。 ### 2.HashMap原理 [https://blog.csdn.net/u014401141/article/details/71030229]: HashMap是基于哈希表的Map接口的非同步实现,继承自AbstractMap,AbstractMap是部分实现Map接口的抽象类。在平时的开发中,HashMap的使用还是比较多的。我们知道ArrayList主要是用数组来存储元素的,LinkedList是用链表来存储的,那么HashMap的实现原理是什么呢?先看下面这张图: ![img](https://imgconvert.csdnimg.cn/aHR0cDovL3N0YXRpYy5vcGVuLW9wZW4uY29tL2xpYi91cGxvYWRJbWcvMjAxNjA5MTgvMjAxNjA5MTgxMDU2NTVfMTIuanBn?x-oss-process=image/format,png) 在之前的版本中,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当链表中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。put(K key,V value)方法中可知,当要存储Key---->Value对时,实际上是存储在一个Entry的对象e中,程序通过key计算出Entry对象的存储位置。换句话说,Key---->Value的对应关系是通过key----Entry----value这个过程实现的,所以就有我们表面上知道的key存在哪里,value就存在哪里。在Map接口中,有一个Entry接口,该接口用于处理key和value的set()和get()方法,所以在Map中存储数据,实际上是将Key---->value的数据存储在Map.Entry接口的实例中,再在Map集合中插入Map.Entry的实例化对象,如图示: ![img](https://imgconvert.csdnimg.cn/aHR0cDovL2RsLml0ZXllLmNvbS91cGxvYWQvYXR0YWNobWVudC8wMDc1LzcwMjEvYmE4OTVlYmQtYmY0MC0zMjM0LWJkMDYtZGM4MGFiZDFmZmViLnBuZw?x-oss-process=image/format,png) ### 3.ConcurrentHashMap [https://blog.csdn.net/u014401141/article/details/81258753]: ConcurrentHashMap是线程安全的: JDK1.7版本: 容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想,见下图: ![img](https://img-blog.csdnimg.cn/20200305160036451.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MDExNDE=,size_16,color_FFFFFF,t_70) ![img](https://img-blog.csdnimg.cn/20200305160044708.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MDExNDE=,size_16,color_FFFFFF,t_70) 每一个segment都是一个HashEntry[] table, table中的每一个元素本质上都是一个HashEntry的单向队列(原理和hashMap一样)。比如table[3]为首节点,table[3]->next为节点1,之后为节点2,依次类推。 ```java public class ConcurrentHashMap extends AbstractMap implements ConcurrentMap, Serializable { // 将整个hashmap分成几个小的map,每个segment都是一个锁;与hashtable相比,这么设计的目的是对于put, remove等操作,可以减少并发冲突,对 // 不属于同一个片段的节点可以并发操作,大大提高了性能 final Segment[] segments; // 本质上Segment类就是一个小的hashmap,里面table数组存储了各个节点的数据,继承了ReentrantLock, 可以作为互拆锁使用 static final class Segment extends ReentrantLock implements Serializable { transient volatile HashEntry[] table; transient int count; } // 基本节点,存储Key, Value值 static final class HashEntry { final int hash; final K key; volatile V value; volatile HashEntry next; } } ``` JDK1.8版本:做了2点修改,见下图: 取消segments字段,直接采用transient volatile HashEntry[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用Synchronized和CAS来操作 将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构. ![img](https://img-blog.csdnimg.cn/20200305160111956.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MDExNDE=,size_16,color_FFFFFF,t_70) 在ConcurrentHashMap中通过一个Node[]数组来保存添加到map中的键值对,而在同一个数组位置是通过链表和红黑树的形式来保存的。但是这个数组只有在第一次添加元素的时候才会初始化,否则只是初始化一个ConcurrentHashMap对象的话,只是设定了一个sizeCtl变量,这个变量用来判断对象的一些状态和是否需要扩容,后面会详细解释。 第一次添加元素的时候,默认初期长度为16,当往map中继续添加元素的时候,通过hash值跟数组长度取与来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了8个以上,如果数组的长度还小于64的时候,则会扩容数组。如果数组的长度大于等于64了的话,在会将该节点的链表转换成树。 通过扩容数组的方式来把这些节点给分散开。然后将这些元素复制到扩容后的新的数组中,同一个链表中的元素通过hash值的数组长度位来区分,是还是放在原来的位置还是放到扩容的长度的相同位置去 。在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表。 取元素的时候,相对来说比较简单,通过计算hash来确定该元素在数组的哪个位置,然后在通过遍历链表或树来判断key和key的hash,取出value值。 **基本过程:** 1.当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了, 2.如果没有初始化就先调用initTable()方法来进行初始化过程 3. 然后通过计算hash值来确定放在数组的哪个位置 1.如果没有hash冲突就直接CAS插入,如果hash冲突的话,则取出这个节点来* 2.如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制 3. 最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作 4. 然后判断当前取出的节点位置存放的是链表还是树 5.如果是链表的话,则遍历整个链表,直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话, 6.则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾 7.如果是树的话,则调用putTreeVal方法把这个元素添加到树中去 8.最后在添加完成之后,调用addCount()方法统计size,判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话, 9. 则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组 ### 4.Set [https://blog.csdn.net/u014401141/article/details/71026561]: 一个不包含重复元素的collection。 Set集合的特点 无序(存储顺序和取出顺序不一致),唯一 HashSet集合 A:底层数据结构是哈希表(是一个元素为链表的数组) B:哈希表底层依赖两个法:hashCode()和equals() **hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable。** **LinkedHashSet** LinkedHashSet:底层数据结构由哈希表和链表组成。 哈希表保证元素的唯一性。 链表保证元素有素。(存储和取出是一致) TreeSet集合 A:底层数据结构是红黑树(是一个自平衡的二叉树) ### 4.Queue 列是一种先进先出的数据结构,元素在队列末尾添加,在队列头部删除。Queue接口扩展自Collection,并提供插入、提取、检验等操作. 接口Deque,是一个扩展自Queue的双端队列,它支持在两端插入和删除元素,因为LinkedList类实现了Deque接口,所以通常我们可以使用LinkedList来创建一个队列。 ### 5.equals()与hashCode() equals():反映的是对象或变量具体的值,即两个对象里面包含的值--可能是对象的引用,也可能是值类型的值。 hashCode():计算出对象实例的哈希码,并返回哈希码,又称为散列函数。根类Object的hashCode()方法的计算,依赖于对象实例的iD(内存地址),故每个Object对象的hashCode都是唯一的;当然,当对象所对应的类重写了 hashCode()方法时,结果就截然不同了。 之所以有hashCode方法,它的主要作用是为了配合基于散列的集合一起正常运行,提高查找效率,是因为在批量的对象比较中,hashCode要比equals来得快,很多集合都用到了hashCode,比如HashTable。 两个obj,如果equals()相等,hashCode()一定相等。 两个obj,如果hashCode()相等,equals()不一定相等(Hash散列值有冲突的情况,虽然概率很低)。 所以: 可以考虑在集合中,判断两个对象是否相等的规则是: 第一步,如果hashCode()相等,则查看第二步,否则不相等; 第二步,查看equals()是否相等,如果相等,则两obj相等,否则还是不相等。 Java语言对equals()的要求如下,这些要求是必须遵循的:   A 对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。   B 反射性:x.equals(x)必须返回是“true”。   C 类推性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。   D 一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。   任何情况下,x.equals(null),永远返回是“false”;x.equals(和x不同类型的对象)永远返回是“false”。 二、equals()相等的两个对象,hashcode()一定相等;   反过来:hashCode()不等,一定能推出equals()也不等;   hashcode()相等,equals()可能相等,也可能不等。 **2.为什么选择hashCode方法** hashCode主要为了提高查询效率 以java.lang.Object来理解,JVM每new一个Object,它都会将这个Object丢到一个Hash哈希表中去,这样的话, 下次做Object的比较或者取这个对象的时候,它会根据对象的hashCode再从Hash表中取这个对象。这样做的目的是提高取对象的效率。 换句话说:当我们重写一个对象的equals方法,就必须重写他的hashCode方法,如果不重写他的hashCode方法的话, Object对象中的hashCode方法始终返回的是一个对象的hash地址,而这个地址是永远不相等的。所以这时候即使是重写了equals方法,也不会有特定的效果的,因为hashCode方法如果都不想等的话,就不会调用equals方法进行比较了,所以没有意义了。 **改写equals时总是要改写hashCode** java.lang.Object中对hashCode的约定: 1. 在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。 2. 如果两个对象根据equals(Object o)方法是相等的,则调用这两个对象中任一对象的hashCode方法必须产生相同的整数结果。 3. 如果两个对象根据equals(Object o)方法是不相等的,则调用这两个对象中任一个对象的hashCode方法,不要求产生不同的整数结果。但如果能不同,则可以提高散列表的性能。 ### 6.List、 Set 和 Map 的初始容量和加载因子 **List** ArrayList 的初始容量是10,加载因子为0.5;扩容增量:原容量的 0.5倍+1;一次扩容后长度为16。 Vector 初始容量为10,加载因子1。扩容增量:原容量的1倍,一次扩容后的容量为20。 **Set** HashSet,初始容量为16,加载因子为0.75;扩容增量:原容量的1.6倍;如 HAshSet 的容量为16,一次扩容后容量为32 **Map** HashMap,初始容量16,加载因子0.75;扩容增量:原容量的1倍;如 HashMap 的容量为16,一次扩容后容量为 32 ## 1.2 java反射 程序运行期间发现更多的类及其属性的机制。 通过反射机制访问java对象的属性,方法,构造方法等。 Java程序中的各个Java类属于同一类事物,描述这类事物的Java类名就是Class Class类代表Java类,它的各个实例对象代表各类的字节码,请注意一个Class对象实际上表示的是一个类型。而这个类型未必一定是一种类,例如int不是类,但int.class是一个Class类型的对象。 Class对象实际上是个泛型类,例如 Employee.class的类型Class. 获取Class实例方式 对象.getClass() Class.forName("类名") 类名.class ## 1.3 java序列化 Java序列化是指把Java对象转换为二进制字节码的过程,而Java反序列化是指把二进制字节码恢复为 Java对象的过程; 序列化和反序列化的过程就是生成和解析上述字符的过程! 目的 保存和传递对象 序列化算法一般步骤 (1)将对象实例相关的类元数据输出。 (2)递归地输出类的超类描述直到不再有超类。 (3)类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。 (4)从上至下递归输出实例的数据 Java如何实现序列化和反序列化 1、JDK类库中序列化和反序列化API (1)java.io.ObjectOutputStream:表示对象输出流; 它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目 标输出流中; (2)java.io.ObjectInputStream:表示对象输入流; 它的readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回。 不是反射 实现序列化的要求 只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常! 相关注意事项 1、序列化时,只对对象的状态进行保存,而不管对象的方法; 2、当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口; 3、当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化; 4、并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如: 安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件, 或者进行RMI传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的; 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重 新的资源分配,而且,也是没有必要这样实现; 5、声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对 象的临时数据。 6、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序 列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明 确的值。显式地定义serialVersionUID有两种用途: 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的 serialVersionUID; 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的 serialVersionUID。 7、Java有很多基础类已经实现了serializable接口,比如String,Vector等。但是也有一些没有实现 serializable接口的; 8、如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决 深拷贝的重要原因; serialVersionUID 序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化 过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的 值。显式地定义serialVersionUID有两种用途: 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的 serialVersionUID; 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的 serialVersionUID。 # 2.并发编程 ## 1.线程生命周期 ### 1.1 线程创建 **1.通过继承Thread类本身** **2.通过实现Runable接口** 将并行运行的任务与运行机制解耦,如果很多任务,要为每个任务创建一个独立的线程所付出的代价太大。 **3.使用callable和Future创建线程** Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。 Callable和Runnable有几点不同: (1)Callable规定的方法是call(),而Runnable规定的方法是run(). (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。 (3)call()方法可抛出异常,而run()方法是不能抛出异常的。 (4)运行Callable任务可拿到一个Future对象, Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。 通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果 `// 1.创建Callable实现类的实例` `CallableFutureTask tCallableFutureTask = new CallableFutureTask();` `// 2.使用FutureTask类来包装Callable对象。` `FutureTask task = new FutureTask(tCallableFutureTask);` `// 3.使用FutureTask对象作为Thread对象参数创建并启动线程` `// 实际还是以Callable来创建对象。并启动线程。。` `new Thread(task).start();` `// 4.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。` `String rs = (String) task.get();` `System.out.println(main最终返回值:----+ rs);` 使用委派设计模式delegate FutureTask详解: https://blog.csdn.net/u014401141/article/details/104474640 ### 1.2 线程状态的转换 **1、新建状态(New)**:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start()这个线程。 **2、就绪状态(Runnable)**:线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。 **3、运行状态(Running**):就绪状态的线程获取了CPU资源,执行程序代码。 **4、阻塞状态(Blocked)**:阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种: (一)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。 (二)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。 (三)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 **5、死亡状态(Dead**):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。 **6.Thread.yield()**方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果) yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。 结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。 ![img](https://img-blog.csdn.net/20180122111108645?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxNDQwMTE0MQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) ![img](https://img-blog.csdn.net/20180122111010465?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxNDQwMTE0MQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) ## 2.线程安全 **线程同步** 1、线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏。 2、线程同步方法是通过锁来实现,每个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。 3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。 4、对于同步,要时刻清醒在哪个对象上同步,这是关键。 5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。 6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。 7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。 ### 2.1 锁 #### 2.1.1 锁分类 **公平锁/非公平锁** 指多个线程按照申请锁的顺序来获取锁 ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁,Synchronized而言,也是一种非公平锁 **可重入锁** 指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。 核心就是用计数器记录当前线程获取锁的次数,进去加一,出去减一 **偏向锁/轻量级锁/重量级锁** 这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。 **偏向锁**是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。 **轻量级锁**是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。 **重量级锁**是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 **自旋锁** 自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU **独享锁/共享锁** 独享锁是指该锁一次只能被一个线程所持有。 共享锁是指该锁可被多个线程所持有。 对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于Synchronized而言,当然是独享锁。 **互斥锁/读写锁** 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是ReentrantLock 读写锁在Java中的具体实现就是ReadWriteLock **乐观锁/悲观锁** 悲观锁:对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制。 乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。 ### 2.2 Synchronized关键字 **锁对象** 普通同步方法,锁是当前实例对象 静态同步方法,锁是当前类的class对象 同步方法块,锁是括号里面的对象 **实现原理**: JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的互斥锁(Mutex Lock) 实现。 Java提供了一种内置的锁机制来支持原子性,每一个Java对象都可以用作一个实现同步的锁,称为内置锁,线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁 内置锁为互斥锁,即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁 具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。 对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。 synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。 如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。 根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。 如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。 如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。在虚拟机规范对monitorenter和monitorexit的行为描述中,有两点是需要特别注意的。 首先,synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。 其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。 Java对象头 synchronized的锁就是保存在Java对象头中的, 包括两部分数据 Mark Word(标记字段):Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳。 Klass Pointer(类型指针) monitor:Owner,初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL。 ![img](http://cdn.processon.com/5e708bbfe4b03b99651ebcbe?e=1584437711&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:_ti5yNl7lBUTZesqPREAh81HuIM=) **锁优化** 锁的三种状态 这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。 **偏向锁**:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。 为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,主要尽可能避免不必须要的CAS操作,如果竞争锁失败,则升级为轻量级锁。 **轻量级锁**:轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。 通过CAS来获取锁和释放锁。 性能依据:对于绝大部分的锁,在整个生命周期内都是不会存在竞争的 缺点:在多线程环境下,其运行效率比重量级锁还会慢 **重量级锁**:重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 锁升级策略: https://www.processon.com/view/5ef83ec65653bb2925b99219 ![img](http://cdn.processon.com/5f2e2a71f346fb7184646d4c?e=1596864641&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:Wf5vWOGQm6u62kUbo3niLzYJOsU=) ### 2.3 重入锁ReentrantLock(1.5) java.util.concurrent(下文称J.U.C)包中的重入锁ReentrantLock来实现同步,在基本用法上,ReentrantLock与Synchronized很相似,他们都具备一样的线程重入特性,只是代码写法上有点区别。 一个表现为API层面的互斥锁lock()和unlock()方法配合try/finally语句块来完成,另一个表现为原生语法层面的互斥锁。 不过,相synchronized,ReentrantLock增加了一些高级功能, 主要有以下3项: **1.等待可中断。** **2.可实现公平锁。** **3.以及锁可以绑定多个条件。** Reentrant 锁有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。 4.Condition实现部分通知,特定通知。 底层采用AQS实现,通过内部Sync继承AQS 可重入锁,是一种递归无阻塞的同步机制 ### 2.4 volatile关键字 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 2)禁止进行指令重排序。 拥有多个线程之间的可见性,不是不具备同步性(也就是原子性)。 **volatile关键字只具有可见性,没有原子性**。 在前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。 要解决这个问题,把该变量声明为volatile(不稳定的)即可,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。 Volatile修饰的成员变量在每次被线程访问时,**都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。 用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新的值。** **使用volatile变量的第二个语义是禁止指令重排序优化**,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。而使用volatile关键字,那为何说它禁止指令重排序呢?从硬件架构上讲,指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。 但并不是说指令任意重排,CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。使用volatile变量的会禁止它。 ![img](http://cdn.processon.com/5dbab38be4b0c5553748ce8f?e=1572520347&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:WRi6BlVLGsaAwXse3orPgGfH-uM=) ### 2.5 ThreadLocal关键字 ThreadLocal,连接ThreadLocalMap和Thread。来处理Thread的TheadLocalMap属性,包括init初始化属性赋值、get对应的变量,set设置变量等。通过当前线程,获取线程上的ThreadLocalMap属性,对数据进行get、set等操作。   ThreadLocalMap,用来存储数据,采用类似hashmap机制,存储了以threadLocal为key,需要隔离的数据为value的Entry键值对数组结构。   ThreadLocal,有个ThreadLocalMap类型的属性,存储的数据就放在这儿。 ThreadLocal、ThreadLocal、Thread之间的关系   ThreadLocalMap是ThreadLocal内部类,由ThreadLocal创建,Thread有ThreadLocal.ThreadLocalMap类型的属性。 ## 3.线程通信 ### 1.wait/notify/notifyAll 1.wait使线程停止运行,而notify使停止的线程继续运行。 2.wait方法可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。 3.notify方法可以随机唤醒等待队列中等待同一共享资源的“一个”线程,并使该线程退出等待队列,进入可运行状态,也就是 notify方法仅通知“一个”线程。 4.notifyAll方法可以使所有正在等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,因为这要取决于JVM虚拟机的实现。 5.方法Wait()锁释放与 notify()锁不释放。当方法 wait被执行后,锁被自动释放,但执行完 notify方法,锁却不自动释放。必须执行完 notify方法所在的同步 synchronized代码块后才释放锁。得到锁的线程不会立即运行,变成运行态。 ### 2.join 方法join的作用是使所属的线程对象x正常执行run方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。方法join具有使线程排队运行的作用,有些类似同步的运行效果。 join与synchronized的区别是:join在内部使用wait方法进行等待,而 sychronized关键字使用的是“对象监器”原理做为同步。 ## 4.java多线程-内存模型 Java内存模型的主要目标是即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节. Java内存模型 内存、 工作内存与Java内存区域中的Java堆、 栈、 方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,一个是内存区域,一个是内存模型。 读取: 1、read:读取主内存的变量,传送到工作内存中。 2、load: 把刚读取的变量,放入到工作内存的变量副本中。 修改: 3、use:把工作内存变量的值传递给执行引擎 4、assign: 把执行引擎收到的值赋值给工作内存的变量 写入: 5、store:把工作内存的变量传送会主内存中 6、write:把刚store的变量放入到主内存中 锁定: 除了以上三种分类,还有锁定操作,用来处理线程独占状态。 lock:把主内存的一个变量锁定。 unlock: 把主内存内,lock的变量释放解锁,释放后可以被其他线程访问。 可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。 Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。 因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final。 同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见final字段的值。 ![img](http://cdn.processon.com/5dbb9b77e4b0335f1e490c1c?e=1572579719&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:ATSwYhMsdvyAtX1CHMeJQqcWhvo=) ![img](http://cdn.processon.com/5dbb9b8de4b002a645d53ed1?e=1572579742&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:adBm-NIn3hjCsptUm9ich49A7Y0=) ## 6.Java并发集合 ### 1. ConcurrentHashMap ### 2. ConcurrentLinkedQueue ### 3. ConcurrentSkipListMap ### 4. ConcurrentSkipListSet ### 5. CopyOnWriteArrayList ### 6. Collections.synchronizedList ## 7.线程池 好处: 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行 提高线程的可管理性,进行统一分配、调优和监控 Java线程池 :https://blog.csdn.net/u014401141/article/details/79140947 Executor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newScheduledThreadPool、newFixedThreadPool、newCachedThreadPool方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池 ThreadPoolExecutor继承了AbstractExecutorService,AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。 而ExecutorService又是继承了Executor接口。 Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;然后ThreadPoolExecutor继承了类AbstractExecutorService。 在ThreadPoolExecutor类中有几个非常重要的方法: execute() submit() shutdown() shutdownNow() ![img](http://cdn.processon.com/5e7182c7e4b06b852fe31f3c?e=1584500951&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:Xr-ntgBempW4v9x3rLh33_OuWfI=) ### 1.线程池原理剖析 默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。 1.如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务; 2.如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务; 3.如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理; 4.如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。 ![img](http://cdn.processon.com/5e718518e4b08e4e24289f55?e=1584501544&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:rUKguu_pKyxnJSfrfYeqD6BoRwQ=) ![img](http://cdn.processon.com/5e718505e4b03b99652011b9?e=1584501525&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:RKTzmSdnADQ_D2fMBG9TMY_sz4U=) ### 2. Executor Executors:静态工厂类,提供了Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 、Callable 等类的静态工厂方法 ThreadPoolExecutor: 参数含义: corePoolSize:线程池中核心线程的数量 maximumPoolSize:线程池中允许的最大线程数 keepAliveTime:线程空闲的时间 unit:keepAliveTime的单位 workQueue:当线程数目超过核心线程数时用于保存任务的队列。主要有3种类型的BlockingQueue可供选择:无界队列,有界队列和同步移交。 用来保存等待执行的任务的阻塞队列: 使用的阻塞队列:ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue,PriorityBlockingQueue threadFactory:用于设置创建线程的工厂,DefaultThreadFactory handler: RejectedExecutionHandler,线程池的拒绝策略 AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 CallerRunsPolicy:由调用线程处理该任务 DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) DiscardPolicy:也是丢弃任务,但是不抛出异常。 线程池分类: newFixedThreadPool: 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数nThreads线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新的线程将代替它执行后续任务(如果需要)。在某个线程被显示关闭之前,池中的线程将一直存在。 可重用固定线程数的线程池,corePoolSize和maximumPoolSize一致,使用“无界”队列LinkedBlockingQueue,maximumPoolSize、keepAliveTime、RejectedExecutionHandler 无效 newSingleThreadExecutor: 创建一个单线程的线程池。这个线程只有一个线程在工作,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个唯一的线程因为异常而结束,那么会有一个新的线程来代替它。此线程保证所有的任务的执行顺序按照任务的提交顺序执行。 使用单个worker线程的Executor,corePoolSize和maximumPoolSize被设置为1,SynchronousQueue作为WorkerQueue。 newCachedThreadPool: 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。对于执行很多短期异步任务而言,这些线程池通常可提供程序性能。调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60s未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源 会根据需要创建新线程的线程池 ,corePoolSize被设置为0,maximumPoolSize被设置为Integer.MAX_VALUE,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程 ,可能会耗尽CPU和内存资源,使用LinkedBlockingQueue作为workerQueue。 Executor.execute() execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。 ExecutorService.submit() submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。 ScheduledThreadPoolExecutor 创建一个定长线程池,支持定时及周期性任务执行 继承自ThreadPoolExecutor,给定的延迟之后运行任务,或者定期执行任务,内部使用DelayQueue来实现 ,会把调度的任务放入DelayQueue中。DelayQueue内部封装PriorityQueue,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序。 ### 3. 线程池调优 CPU密集 CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。 CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。 CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数. IO密集 IO密集型,即该任务需要大量的IO,即大量的阻塞。 在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。 IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1. 混合型的任务 混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。 最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目 原生线程池这么强大,Tomcat 为何还需扩展线程池? JDK 实现线程池功能比较完善,但是比较适合运行 CPU 密集型任务,不适合 IO 密集型的任务。对于 IO 密集型任务可以间接通过设置线程池参数方式做到。 ## 8.并发工具类 ### 1.AQS AbstarctQueuedSynchronizer简称AQS,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建的,例如ReentrantLock,Semphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时大量的细节问题,例如获取同步状态、FIFO同步队列 采用模板方法模式,AQS实现大量通用方法,子类通过继承方式实现其抽象方法来管理同步状态 设计思想: 同步器的核心方法是acquire和release操作,其背后的思想也比较简洁明确 从这两个操作中的思想中我们可以提取出三大关键操作:同步器的状态变更、线程阻塞和释放、插入和移出队列。所以为了实现这两个操作,需要协调三大关键操作引申出来的三个基本组件: acquire: acquire操作是这样的: while (当前同步器的状态不允许获取操作){ 如果当前线程不在队列中,则将其插入 队列阻塞当前线程 } 如果线程位于队列中,则将其移出队列 release: 1.更新同步器的状态 2.if(新的状态允许某个被阻塞的线程获取成功){ 解除队列中一个或多个线程的阻塞状态 } 三大关键操作: 同步器的状态变更 AQS类使用单个int(32位)来保存同步状态,并暴露出getState、setState以及compareAndSet操作来读取和更新这个同步状态。其中属性state被声明为volatile,并且通过使用CAS指令来实现compareAndSetState,使得当且仅当同步状态拥有一个一致的期望值的时候,才会被原子地设置成新值,这样就达到了同步状态的原子性管理,确保了同步状态的原子性、可见性和有序性。 同步状态获取与释放 独占式 获取锁 获取同步状态:acquire 响应中断:acquireInterruptibly 超时获取:tryAcquireNanos 释放锁:release 共享式 获取锁------------- acquireShared 释放锁-------------releaseShared 具体锁的获取和释放流程 独占锁的获取和释放流程 获取 1.调用入口方法acquire(arg) 2.调用模版方法tryAcquire(arg)尝试获取锁,若成功则返回,若失败则走下一步 3.将当前线程构造成一个Node节点,并利用CAS将其加入到同步队列到尾部,然后该节点对应到线程进入自旋状态 4.自旋时,首先判断其前驱节点释放为头节点&是否成功获取同步状态,两个条件都成立,则将当前线程的节点设置为头节点,如果不是,则利用LockSupport.park(this)将当前线程挂起 ,等待被前驱节点唤醒 释放 1.调用入口方法release(arg) 2.调用模版方法tryRelease(arg)释放同步状态 3.获取当前节点的下一个节点,利用LockSupport.unpark(currentNode.next.thread)唤醒后继节点(接获取的第四步) 共享锁的获取和释放流程 ######获取锁 调用acquireShared(arg)入口方法 进入tryAcquireShared(arg)模版方法获取同步状态,如果返返回值>=0,则说明同步状态(state)有剩余,获取锁成功直接返回 如果tryAcquireShared(arg)返回值<0,说明获取同步状态失败,向队列尾部添加一个共享类型的Node节点,随即该节点进入自旋状态 自旋时,首先检查前驱节点释放为头节点&tryAcquireShared()是否>=0(即成功获取同步状态) 如果是,则说明当前节点可执行,同时把当前节点设置为头节点,并且唤醒所有后继节点 如果否,则利用LockSupport.unpark(this)挂起当前线程,等待被前驱节点唤醒 ######释放锁 调用releaseShared(arg)模版方法释放同步状态 如果释放成功,则遍历整个队列,利用LockSupport.unpark(nextNode.thread)唤醒所有后继节点 独占锁和共享锁在实现上的区别 1.独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态 共享锁的同步状态>1,取值由上层同步组件确定 2.独占锁队列中头节点运行完成后释放它的直接后继节点 共享锁队列中头节点运行完成后释放它后面的所有节点 3.共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况 线程阻塞和唤醒 LockSupport.park阻塞当前线程直到有个LockSupport.unpark方法被调用。unpark的调用是没有被计数的,因此在一个park调用前多次调用unpark方法只会解除一个park操作。另外,它们作用于每个线程而不是每个同步器。一个线程在一个新的同步器上调用park操作可能会立即返回,因为在此之前可以有多余的unpark操作。但是,在缺少一个unpark操作时,下一次调用park就会阻塞。虽然可以显式地取消多余的unpark调用,但并不值得这样做。在需要的时候多次调用park会更高效。park方法同样支持可选的相对或绝对的超时设置,以及与JVM的Thread.interrupt结合 ,可通过中断来unpark一个线程。 当有线程获取锁了,其他再次获取时需要阻塞,当线程释放锁后,AQS负责唤醒线程 LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语 每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在进程中使用,则调用park()将会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用 unpark 使其可用 park()、unpark() 插入和移出队列 同步队列是AQS很重要的组成部分,它是一个双端队列,遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会被构造成一个Node节点假如到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用。 CLH同步队列 FIFO双向队列,AQS依赖它来解决同步状态的管理问题 首节点唤醒,等待队列加入到CLH同步队列的尾部 ![img](http://cdn.processon.com/5e741ea4e4b08e4e242e1a0c?e=1584671924&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:TfNx4YHZfUsDTbsN7TXXeMakd7M=) ![img](http://cdn.processon.com/5ec64b86e0b34d5f26212c9e?e=1590057366&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:9cSmlMwHqJdAR3MC_xqtuODkbuo=) 读写锁 Java提供了一个基于AQS到读写锁实现ReentrantReadWriteLock,该读写锁到实现原理是:将同步变量state按照高16位和低16位进行拆分,高16位表示读锁,低16位表示写锁。 ### 2.CAS Compare And Swap,整个JUC体系最核心、最基础理论 内存值V、旧的预期值A、要更新的值B,当且仅当内存值V的值等于旧的预期值A时才会将内存值V的值修改为B,否则什么都不干 native中存在四个参数 缺陷: 循环时间太长 只能保证一个共享变量原子操作 ABA问题: 解决方案:版本号,AtomicStampedReference ### 3.并发工具类 https://blog.csdn.net/u014401141/article/details/102870572 # 3.JVM ## 3.1 JAVA内存区域 ### 1.基本问题 - **介绍下 Java 内存区域(运行时数据区)** - **Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)** - **对象的访问定位的两种方式(句柄和直接指针两种方式)** - **String 类和常量池** - **8 种基本类型的包装类和常量池** ### 2. 运行时数据区域 Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同,下面会介绍到。 **JDK 1.8 之前:** ![img](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/java内存区域/JVM运行时数据区域.png) **JDK 1.8 :** ![img](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/java内存区域/2019-3Java运行时数据区域JDK1.8.png) **线程私有的:** - 程序计数器 - 虚拟机栈 - 本地方法栈 **线程共享的:** - 堆 - 方法区 - 直接内存 (非运行时数据区的一部分) #### 2.1 程序计数器 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。**字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。** 另外,**为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。** **从上面的介绍中我们知道程序计数器主要有两个作用:** 1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 **注意:程序计数器是唯一一个不会出现 `OutOfMemoryError` 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。** #### 2.2 Java 虚拟机栈 **与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。** **Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。** (实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。) **局部变量表主要存放了编译期可知的各种数据类型**(boolean、byte、char、short、int、float、long、double)、**对象引用**(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 **Java 虚拟机栈会出现两种错误:`StackOverFlowError` 和 `OutOfMemoryError`。** - **`StackOverFlowError`:** 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。 - **`OutOfMemoryError`:** 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。 Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。 **扩展:那么方法/函数如何调用?** Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。 Java 方法有两种返回方式: 1. return 语句。 2. 抛出异常。 不管哪种返回方式都会导致栈帧被弹出。 #### 2.3 本地方法栈 和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。 #### 2.4 堆 Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。** **Java世界中“几乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。** Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(Garbage Collected Heap)**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。** 在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分: 1. 新生代内存(Young Generation) 2. 老生代(Old Generation) 3. 永生代(Permanent Generation) ![JVM堆内存结构-JDK7](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/java内存区域/JVM堆内存结构-JDK7.png) JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。 ![JVM堆内存结构-JDK8](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/java内存区域/JVM堆内存结构-jdk8.png) **上图所示的 Eden 区、两个 Survivor 区都属于新生代(为了区分,这两个 Survivor 区域按照顺序被命名为 from 和 to),中间一层属于老年代。** 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 > 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。 > > **动态年龄计算的代码如下** > > ```c++ > uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { > //survivor_capacity是survivor空间的大小 > size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); > size_t total = 0; > uint age = 1; > while (age < table_size) { > total += sizes[age];//sizes数组是每个年龄段对象大小 > if (total > desired_survivor_size) break; > age++; > } > uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; > ... > } > ``` 堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如: 1. **`OutOfMemoryError: GC Overhead Limit Exceeded`** : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。 2. **`java.lang.OutOfMemoryError: Java heap space`** :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发`java.lang.OutOfMemoryError: Java heap space` 错误。(和本机物理内存无关,和你配置的内存大小有关!) 3. ...... #### 2.5 方法区 方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 **Java 虚拟机规范把方法区描述为堆的一个逻辑部分**,但是它却有一个别名叫做 **Non-Heap(非堆)**,目的应该是与 Java 堆区分开来。 方法区也被称为永久代。很多人都会分不清方法区和永久代的关系,为此我也查阅了文献。 ##### 2.5.1 方法区和永久代的关系 > 《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 **方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。** 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。 ##### 2.5.2 常用参数 JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小 ```java -XX:PermSize=N //方法区 (永久代) 初始大小 -XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen ``` 相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。 JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。 下面是一些常用参数: ```java -XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小) -XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小 ``` 与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。 ##### 2.5.3 为什么要将永久代 (PermGen) 1. 整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。 > 当你元空间溢出时会得到如下错误: `java.lang.OutOfMemoryError: MetaSpace` 你可以使用 `-XX:MaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。`-XX:MetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。 1. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 `MaxPermSize` 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。 2. 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。 #### 2.6 运行时常量池 运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用) 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。 ~~**JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。**~~ > 修正([issue747](https://github.com/Snailclimb/JavaGuide/issues/747),[reference](https://blog.csdn.net/q5706503/article/details/84640762)): > > 1. **JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代** > 2. **JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代** 。 > 3. **JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)** 相关问题:JVM 常量池中存储的是对象还是引用呢?: https://www.zhihu.com/question/57109429/answer/151717241 by RednaxelaFX #### 2.7 直接内存 **直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。** JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**通道(Channel)** 与**缓存区(Buffer)** 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为**避免了在 Java 堆和 Native 堆之间来回复制数据**。 本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。 ### 3. 对象的创建 下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。 ![Java创建对象的过程](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/java内存区域/Java创建对象的过程.png) #### Step1:类加载检查 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 #### Step2:分配内存 在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。 **内存分配的两种方式:(补充内容,需要掌握)** 选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的 ![内存分配的两种方式](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/java内存区域/内存分配的两种方式.png) **内存分配并发问题(补充内容,需要掌握)** 在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全: - **CAS+失败重试:** CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。**虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。** - **TLAB:** 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配 #### Step3:初始化零值 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 #### Step4:设置对象头 初始化零值完成之后,**虚拟机要对对象进行必要的设置**,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 **这些信息存放在对象头中。** 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 #### Step5:执行 init 方法 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,`` 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 `` 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 ### 4. 对象的内存布局 在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:**对象头**、**实例数据**和**对齐填充**。 **Hotspot 虚拟机的对象头包括两部分信息**,**第一部分用于存储对象自身的运行时数据**(哈希码、GC 分代年龄、锁状态标志等等),**另一部分是类型指针**,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 **实例数据部分是对象真正存储的有效信息**,也是在程序中所定义的各种类型的字段内容。 **对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。** 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 ### 5. 对象的访问定位 建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有**①使用句柄**和**②直接指针**两种: 1. **句柄:** 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; ![对象的访问定位-使用句柄](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/java内存区域/对象的访问定位-使用句柄.png) 2. **直接指针:** 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。 ![对象的访问定位-直接指针](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/java内存区域/对象的访问定位-直接指针.png) **这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。** ### 6. String 类和常量池 **String 对象的两种创建方式:** ```java String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd""; String str2 = new String("abcd");//堆中创建一个新的对象 String str3 = new String("abcd");//堆中创建一个新的对象 System.out.println(str1==str2);//false System.out.println(str2==str3);//false ``` 这两种不同的创建方法是有差别的。 - 第一种方式是在常量池中拿对象; - 第二种方式是直接在堆内存空间创建一个新的对象。 记住一点:**只要使用 new 方法,便需要创建新的对象。** 再给大家一个图应该更容易理解,图片来源:https://www.journaldev.com/797/what-is-java-string-pool: ![String-Pool-Java](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/java内存区域/2019-3String-Pool-Java1-450x249.png) **String 类型的常量池比较特殊。它的主要使用方法有两种:** - 直接使用双引号声明出来的 String 对象会直接存储在常量池中。 - 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。 ```java String s1 = new String("计算机"); String s2 = s1.intern(); String s3 = "计算机"; System.out.println(s2);//计算机 System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象, System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象 ``` **字符串拼接:** ```java String str1 = "str"; String str2 = "ing"; String str3 = "str" + "ing";//常量池中的对象 String str4 = str1 + str2; //在堆上创建的新的对象 String str5 = "string";//常量池中的对象 System.out.println(str3 == str4);//false System.out.println(str3 == str5);//true System.out.println(str4 == str5);//false ``` ![字符串拼接](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/java内存区域/字符串拼接-常量池2.png) 尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。 **String s1 = new String("abc");这句话创建了几个字符串对象?** **将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。** **验证:** ```java String s1 = new String("abc");// 堆内存的地址值 String s2 = "abc"; System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。 System.out.println(s1.equals(s2));// 输出 true ``` **结果:** ``` false true ``` **8 种基本类型的包装类和常量池** **Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。** 为啥把缓存设置为[-128,127]区间?([参见issue/461](https://github.com/Snailclimb/JavaGuide/issues/461))性能和资源之间的权衡。 ```java public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); } private static class CharacterCache { private CharacterCache(){} static final Character cache[] = new Character[127 + 1]; static { for (int i = 0; i < cache.length; i++) cache[i] = new Character((char)i); } } ``` 两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。** ```java Integer i1 = 33; Integer i2 = 33; System.out.println(i1 == i2);// 输出 true Integer i11 = 333; Integer i22 = 333; System.out.println(i11 == i22);// 输出 false Double i3 = 1.2; Double i4 = 1.2; System.out.println(i3 == i4);// 输出 false ``` **Integer 缓存源代码:** ```java /** *此方法将始终缓存-128 到 127(包括端点)范围内的值,并可以缓存此范围之外的其他值。 */ public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } ``` **应用场景:** 1. Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。 2. Integer i1 = new Integer(40);这种情况下会创建新的对象。 ```java Integer i1 = 40; Integer i2 = new Integer(40); System.out.println(i1==i2);//输出 false ``` **Integer 比较更丰富的一个例子:** ```java Integer i1 = 40; Integer i2 = 40; Integer i3 = 0; Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0); System.out.println("i1=i2 " + (i1 == i2)); System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); System.out.println("i1=i4 " + (i1 == i4)); System.out.println("i4=i5 " + (i4 == i5)); System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); System.out.println("40=i5+i6 " + (40 == i5 + i6)); ``` 结果: ``` i1=i2 true i1=i2+i3 true i1=i4 false i4=i5 false i4=i5+i6 true 40=i5+i6 true ``` 解释: 语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。 ### 7.类加载机制 #### 1. 类加载器概述   java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制.JVM中用来完成上述功能的具体实现就是**类加载器**.类加载器读取.class字节码文件将其转换成java.lang.Class类的一个实例.每个实例用来表示一个java类.通过该实例的newInstance()方法可以创建出一个该类的对象. #### 2. 类的生命周期   类从加载到虚拟机内存到被从内存中释放,经历的生命周期如下: ![img](http://images2015.cnblogs.com/blog/870109/201605/870109-20160503213708857-429280187.png) **加载**:"加载"是"类加载"过程的一个阶段,此阶段完成的功能是:   通过类的全限定名来获取定义此类的二进制字节流   将此二进制字节流所代表的静态存储结构转化成方法区的运行时数据结构   在内存中生成代表此类的java.lang.Class对象,作为该类访问入口. **验证**:连接阶段第一步.验证的目的是确保Class文件的字节流中信息符合虚拟机的要求,不会危害虚拟机安全,使得虚拟机免受恶意代码的攻击.大致完成以下四个校验动作:   文件格式验证   源数据验证   字节码验证   符号引用验证 **准备**:连接阶段第二步,正式为类变量分配内存并设置变量的初始值.(仅包含类变量,不包含实例变量).   **解析**:连接阶段第三步,虚拟机将常量池中的符号引用替换为直接引用,解析动作主要针对类或接口,字段,类方法,方法类型等等.. **初始化**:类的初始化是类加载过程的最后一步,在该阶段,才真正意义上的开始执行类中定义的java程序代码.该阶段会执行类构造器. **使用**:使用该类所提供的功能. **卸载**:从内存中释放. #### 3. 类加载器的作用 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的[Java](http://lib.csdn.net/base/javase).lang.Class对象,用来封装类在方法区类的对象。看下面图 ![](https://i.loli.net/2020/11/08/fcW2eIbx94RHZBD.jpg) 类的加载的最终产品是位于堆区中的Class对象 Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个类负责加载特定位置的类:BootStrap,ExtClassLoader,AppClassLoader。类加载器也是Java类,因为其他是java类的类加载器本身也要被类加载器加载,显然必须有第一个类加载器不是java类,这正是BootStrap。Java虚拟机中的所有类装载器采用具有父子关系的树形结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类装载器为其父级类加载。 #### 4. 获取Class文件途径 java类可以动态被加载到内存,这是java的一大特点,也称为运行时绑定,或动态绑定. 1.从ZIP包中读取,很常见,最终成为日后JAR,WAR,EAR格式的基础. 2.从网络中获取,这种场景典型的就是Applet. 3.运行时计算生成,典型的情景就是java动态代理技术. 4.从其他文件中生成,典型场景是JSP应用,即由JSP文件生成对应的Class类. #### 5. 类加载器的分类 大部分java程序会使用以下3中系统提供的类加载器: **启动类加载器(Bootstrap ClassLoader):** 这个类加载器负责将\lib目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,代码是使用C++编写的.是虚拟机自身的一部分. **扩展类加载器(Extendsion ClassLoader):** 这个类加载器负责加载\lib\ext目录下的类库,用来加载java的扩展库,开发者可以直接使用这个类加载器. **应用程序类加载器(Application ClassLoader):** 这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器. 除此之外,我们还可以加入自己定义的类加载器,以满足特殊的需求,需要继承java.lang.ClassLoader类.   类加载器之间的层次关系如下图:    ![img](http://images2015.cnblogs.com/blog/870109/201605/870109-20160503202555529-776544675.png) #### 6. 双亲委派模型 双亲委派模型是一种组织类加载器之间关系的一种规范,他的工作原理是: 如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载. 这样的好处是:java类随着它的类加载器一起具备了带有优先级的层次关系.这是十分必要的,比如java.langObject,它存放在\jre\lib\rt.jar中, 它是所有java类的父类,因此无论哪个类加载都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载器中,因此Object类会由启动类加载器来加载,所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载器自行去加载的话,系统中就会出现不止一个Object类,应用程序就会全乱了 #### 7. 程序的赋值步骤 父类的静态变量赋值 自身的静态变量赋值 父类成员变量赋值 父类块赋值 父类构造器赋值 自身成员变量赋值 自身块赋值 自身构造器赋值 #### 8.什么情况下需要开始类加载过程 1.遇到new、get static、put static或invoke static这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。 2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 ## 3.2 垃圾回收 ### 3.2.1 垃圾收集算法 #### 1.标记-清除 此算法执行分两阶段。 第一阶段:从引用根节点开始标记所有被引用的对象。 第二阶段:遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。 **适用场合** 存活对象较多的情况下比较高效 适用于年老代(即旧生代) **缺点** 效率问题 标记和清除的效率都不高 空间问题 大量不连续的内存碎片,这样给大对象分配内存的时候可能会提前触发full gc。 #### 2.复制 将可用内存的容量划分为大小相等的两块,每次只使用其中的一块。 当这一块用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 **适用场合** 适用于年轻代(即新生代):基本上98%的对象是”朝生夕死”的,存活下来的会很少 存活对象较少的情况下比较高效 **缺点** 需要一块儿空的内存空间 需要复制移动对象 #### 3.标记-整理 此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段, 第一阶段从根节点开始标记所有被引用对象。 第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。 **适用场合** 用于年老代(即旧生代) **优点** 不会产生内存碎片 **缺点** 需要移动对象,若对象非常多而且标记回收后的内存非常不完整,可能移动这个动作也会耗费一定时间 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象) #### 4. 分代收集算法 当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 从内存回收的角度来看,所以Java堆中还可以细分 **新生代** **Eden区** Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。 **Survivor From** 上一次GC的幸存者,作为这一次GC的被扫描者。 **Survivor To** 保留了一次MinorGC过程中的幸存者。 MinorGC的过程:MinorGC采用复制算法。首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如ServicorTo不够位置了就放到老年区);然后,清空Eden,ServicorFrom中的对象,最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区 Minor GC触发条件 当Eden区满时 **老年代** 老年代的对象比较稳定,所以MajorGC(full GC)不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。 MajorGC采用标记—清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。 当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。 **元空间(永久代)** 指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域. 它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。 在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制. ### 3.2.2 对象内存分配和回收策略 Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 **堆** 内存中对象的分配与回收。 Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(Garbage Collected Heap)**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。** **堆空间的基本结构:** ![img](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/jvm垃圾回收/01d330d8-2710-4fad-a91c-7bbbfaaefc0e.png) 上图所示的 Eden 区、From Survivor0("From") 区、To Survivor1("To") 区都属于新生代,Old Memory 区属于老年代。 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 > 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。 > > **动态年龄计算的代码如下** > > ```c++ > uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { > //survivor_capacity是survivor空间的大小 > size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); > size_t total = 0; > uint age = 1; > while (age < table_size) { > total += sizes[age];//sizes数组是每个年龄段对象大小 > if (total > desired_survivor_size) break; > age++; > } > uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; > ... > } > ``` 经过这次GC后,Eden区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。 ![堆内存常见分配策略 ](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/jvm垃圾回收/堆内存.png) #### 1. 对象优先在 eden 区分配 目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.下面我们来进行实际测试以下。 **测试:** ```java public class GCTest { public static void main(String[] args) { byte[] allocation1, allocation2; allocation1 = new byte[30900*1024]; //allocation2 = new byte[900*1024]; } } ``` 通过以下方式运行: ![img](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/jvm垃圾回收/25178350.png) 添加的参数:`-XX:+PrintGCDetails` ![img](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/jvm垃圾回收/10317146.png) 运行结果 (红色字体描述有误,应该是对应于 JDK1.7 的永久代): ![img](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-26/28954286.jpg) 从上图我们可以看出 eden 区内存几乎已经被分配完全(即使程序什么也不做,新生代也会使用 2000 多 k 内存)。假如我们再为 allocation2 分配内存会出现什么情况呢? ```java allocation2 = new byte[900*1024]; ``` ![img](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-26/28128785.jpg) **简单解释一下为什么会出现这种情况:** 因为给 allocation2 分配内存的时候 eden 区内存几乎已经被分配完了,我们刚刚讲了当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,所以只好通过 **分配担保机制** 把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 Full GC。执行 Minor GC 后,后面分配的对象如果能够存在 eden 区的话,还是会在 eden 区分配内存。可以执行如下代码验证: ```java public class GCTest { public static void main(String[] args) { byte[] allocation1, allocation2,allocation3,allocation4,allocation5; allocation1 = new byte[32000*1024]; allocation2 = new byte[1000*1024]; allocation3 = new byte[1000*1024]; allocation4 = new byte[1000*1024]; allocation5 = new byte[1000*1024]; } } ``` #### 2. 大对象直接进入老年代 大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。 -XX:PretenureSizeThreshold=3145728 3M **为什么要这样呢?** 为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。 #### 3. 长期存活的对象将进入老年代 既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 #### 4. 动态对象年龄判定 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 > 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。 > > **动态年龄计算的代码如下** > > ```c++ > uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { > //survivor_capacity是survivor空间的大小 > size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); > size_t total = 0; > uint age = 1; > while (age < table_size) { > total += sizes[age];//sizes数组是每个年龄段对象大小 > if (total > desired_survivor_size) break; > age++; > } > uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; > ... > } > ``` 额外补充说明([issue672](https://github.com/Snailclimb/JavaGuide/issues/672)):**关于默认的晋升年龄是15,这个说法的来源大部分都是《深入理解Java虚拟机》这本书。** 如果你去Oracle的官网阅读[相关的虚拟机参数](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html),你会发现`-XX:MaxTenuringThreshold=threshold`这里有个说明 **Sets the maximum tenuring threshold for use in adaptive GC sizing. The largest value is 15. The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6.** #### 5. 引用 强引用 引用在,不回收 软引用 OOM前,把这些对象回收,如果仍OOM→抛出异常 弱引用 下一次GC则清除 虚引用 回收时→系统通知 ## 3.3 垃圾收集器 ![垃圾收集器分类](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/jvm垃圾回收/垃圾收集器.png) **如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。** 虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,**我们能做的就是根据具体应用场景选择适合自己的垃圾收集器**。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。 ### 1. Serial 收集器 Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 **“单线程”** 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( **"Stop The World"** ),直到它收集结束。 **新生代采用复制算法,老年代采用标记-整理算法。** ![ Serial 收集器 ](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/jvm垃圾回收/46873026.png) 虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。 但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它**简单而高效(与其他收集器的单线程相比)**。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。 ### 2. ParNew 收集器 **ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。** **新生代采用复制算法,老年代采用标记-整理算法。** ![ParNew 收集器 ](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/jvm垃圾回收/22018368.png) 它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。 **并行和并发概念补充:** - **并行(Parallel)** :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 - **并发(Concurrent)**:指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。 ### 3. Parallel Scavenge 收集器 Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它看上去几乎和ParNew都一样。 **那么它有什么特别之处呢?** ``` -XX:+UseParallelGC 使用 Parallel 收集器+ 老年代串行 -XX:+UseParallelOldGC 使用 Parallel 收集器+ 老年代并行 ``` **Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。** Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。 **新生代采用复制算法,老年代采用标记-整理算法。** ![Parallel Scavenge 收集器 ](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/jvm垃圾回收/parllel-scavenge收集器.png) **是JDK1.8默认收集器** 使用java -XX:+PrintCommandLineFlags -version命令查看 ``` -XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC java version "1.8.0_211" Java(TM) SE Runtime Environment (build 1.8.0_211-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode) ``` JDK1.8默认使用的是Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC来禁用该功能 ### 4.Serial Old 收集器 **Serial 收集器的老年代版本**,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。 ### 5. Parallel Old 收集器 **Parallel Scavenge 收集器的老年代版本**。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。 ### 6. CMS 收集器 **CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。** **CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。** 从名字中的**Mark Sweep**这两个词可以看出,CMS 收集器是一种 **“标记-清除”算法**实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤: - **初始标记:** 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; - **并发标记:** 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 - **重新标记:** 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 - **并发清除:** 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。 ![CMS 垃圾收集器 ](https://snailclimb.gitee.io/javaguide/docs/java/jvm/pictures/jvm垃圾回收/CMS收集器.png) 从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:**并发收集、低停顿**。但是它有下面三个明显的缺点: - **对 CPU 资源敏感;** - **无法处理浮动垃圾;** - **它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。** ### 7. G1 收集器 **G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.** Garbage first 垃圾收集器是目前垃圾收集器理论发展的前沿成果,相比与CMS 收集器,G1 收集器两个突出的改进是: 1.基于标记-整理算法,不产生内存碎片。 2.可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。 3.G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域 的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得高的垃圾收集效率。 **分区概念** 在G1当中把堆内存分成了一个个Region,新生代,老年代不再是物理上面的分割,而是逻辑上的分割 **分区 Region** G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。 **卡片 Card** 在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。 **堆 Heap** 当Eden区满的时候,会触发一次Minor GC,这种触发机制和之前的其他垃圾收集器差不多,都是将Eden区和其中一个Survivor区的存活对象拷贝到另外一个Survivor区或者晋升到Old 区域,然后清空Eden区和Survivor区。 被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点: - **并行与并发**:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。 - **分代收集**:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。 - **空间整合**:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。 - **可预测的停顿**:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。 G1 收集器的运作大致分为以下几个步骤: 1、初始标记,整个过程STW(是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外),标记了从GC Roots 的可达对象 2、并发标记 ,真个过程用户线程的垃圾回收线程共同执行,标记出GC Roots可达对象的关联对象,收集整个Region的存活对象。 3、最终标记,整个过程STW,标记出并发标记遗漏的,以及引用关系发生变化的存活对象。 4、筛选回收,如果发现完全没有活对象的region就会将其整体回收到可分配region列表中。 **G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)**。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。 **G1提供了三种模式的垃圾回收** 1、young GC(或者叫minor GC):只收集young gen里的所有region,也就是eden和survivor。控制young GC开销的手段是动态改变young region的个数; 当Eden区满的时候,会触发一次Minor GC,这种触发机制和之前的其他垃圾收集器差不多,都是将Eden区和其中一个Survivor区的存活对象拷贝到另外一个Survivor区或者晋升到Old 区域,然后清空Eden区和Survivor区。 2、mixed GC:收集young gen里的所有region,外加若干选定的old gen region。控制mixed GC开销的手段是选多少个、哪几个old gen region。 当越来越多的年轻代中的对象晋升到老年代,会触发一混合的垃圾收集器。也就是除了回收老年代,也会回收年轻代。这里是一部分老年代,可以选择哪些老年代的对象需要收集。 3、Full GC:其实没有Full GC了。G1 GC的控制范围内没有full GC。如果mixed GC无法跟上mutator分配的速度,导致没有足够的空region来完成mixed GC,那么就会使用serial old GC( mark-compact)来对整堆收集一次。 **G1和CMS的比较** CMS收集器是获取最短回收停顿时间为目标的收集器,因为CMS工作时,GC工作线程与用户线程可以并发执行,以此来达到降低手机停顿时间的目的(只有初始标记和重新标记会STW)。但是CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。 CMS仅作用于老年代,是基于标记清除算法,所以清理的过程中会有大量的空间碎片。 CMS收集器无法处理浮动垃圾,由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。 G1是一款面向服务端应用的垃圾收集器,适用于多核处理器、大内存容量的服务端系统。G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短STW的停顿时间,它满足短时间停顿的同时达到一个高的吞吐量。 从JDK 9开始,G1成为默认的垃圾回收器。当应用有以下任何一种特性时非常适合用G1: Full GC持续时间太长或者太频繁; 对象的创建速率和存活率变动很大; 应用不希望停顿时间长(长于0.5s甚至1s)。 G1将空间划分成很多块(Region),然后他们各自进行回收。 堆比较大的时候可以采用,采用复制算法,碎片化问题不严重。 整体上看属于标记整理算法,局部(region之间)属于复制算法。 G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。 而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。 所以 CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB。 jdk7、8、9默认垃圾回收器 jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) jdk1.9 默认垃圾收集器G1 作用的内存分区 ![](https://i.loli.net/2020/11/06/41fBZJaGKSPU2Cv.png) 理解吞吐量和停顿时间 停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验; 高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任 务。 停顿时间->垃圾收集器进行垃圾回收终端应用执行响应的时间 吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间) 如何选择合适的垃圾收集器 优先调整堆的大小让服务器自己来选择 如果内存小于100M,使用串行收集器 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选 如果允许停顿时间超过1秒,选择并行或JVM自己选 如果响应时间最重要,并且不能超过1秒,使用并发收集器 对于G1收集 ## 3.4 常用参数 GC相关的常用参数 -Xmx: 设置堆内存的最大值。 -Xms: 设置堆内存的初始值。 -Xmn: 设置新生代的大小。 -Xss: 设置栈的大小。 -PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。 -MaxTenuringThrehold: 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个参数值时就进入老年代。 -UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。 -SurvivorRattio: 新生代Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Suvivor= 8: 1。 -XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。 -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。 -XX:GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。 ## 3.5 JVM性能优化 1. GC 调优原则; 2. GC 调优目的; 3. GC 调优策略; ### 1.GC 调优原则 在调优之前,我们需要记住下面的原则: > 多数的 Java 应用不需要在服务器上进行 GC 优化; > > 多数导致 GC 问题的 Java 应用,都不是因为我们参数设置错误,而是代码问题; > > 在应用上线之前,先考虑将机器的 JVM 参数设置到最优(最适合); > > 减少创建对象的数量; > > 减少使用全局变量和大对象; > > GC 优化是到最后不得已才采用的手段; > > 在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多。 ### 2.调优目的 将转移到老年代的对象数量降低到最小; 减少 GC 的执行时间。 ### 3.调优策略 **策略 1:**将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。 **策略 2:**大对象进入老年代,虽然大部分情况下,将对象分配在新生代是合理的。但是对于大对象这种做法却值得商榷,大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代,破坏新生代的对象结构,可能会出现频繁的 full gc。因此,对于大对象,可以设置直接进入老年代(当然短命的大对象对于垃圾回收来说简直就是噩梦)。`-XX:PretenureSizeThreshold` 可以设置直接进入老年代的对象大小。 **策略 3:**合理设置进入老年代对象的年龄,`-XX:MaxTenuringThreshold` 设置对象进入老年代的年龄大小,减少老年代的内存占用,降低 full gc 发生的频率。 **策略 4:**设置稳定的堆大小,堆大小设置有两个参数:`-Xms` 初始化堆大小,`-Xmx` 最大堆大小。 **策略5:**注意: 如果满足下面的指标,**则一般不需要进行 GC 优化:** > MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次。 常见提问: **JVM基本结构?** **对象创建的过程?** **类加载机制?** **垃圾回收有什么算法,各有什么特点?** **垃圾回收机制的基本流程?** **各个垃圾收集器的特点,特别是G1收集器的特点?** **JVM怎么去进行性能优化?** **如何判断对象是否死亡(两种方法)。** **简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。** **如何判断一个常量是废弃常量** **方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?** 判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 **“无用的类”** : - 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。 - 加载该类的 `ClassLoader` 已经被回收。 - 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。 HotSpot 为什么要分为新生代和老年代? 进一步划分的目的是更好地回收内存,或者更快地分配内存 常见的垃圾回收器有哪些? 介绍一下 CMS,G1 收集器。 Minor Gc 和 Full GC 有什么不同呢? # 4.网络编程 ## 1.nio,与aio的区别 阻塞,非阻塞:进程/线程要访问的数据是否就绪,进程/线程是否需要等待,强调的数据准备这个过程; 同步,异步:访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞;异步只需要I/O操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写,强调I/O真正读写这个阶段。 NIO为同步非阻塞形式。NIO没有实现异步,在JDK1.7之后,升级了NIO库包,支持异步非阻塞通信模型,即NIO2.0(AIO)。 同步和异步:同步和异步一般是面向操作系统与应用程序对IO操作的层面上来区别的。 ①同步时,应用程序会直接参与IO读写操作,并且应用程序会直接阻塞到某一个方法上,直到数据准备就绪(BIO);或者采用轮询的策略实时检查数据的就绪状态,如果就绪则获取数据(NIO)。 ②异步时,则所有的IO读写操作都交给操作系统处理,与应用程序没有直接关系,应用程序并不关心IO读写,当操作系统完成IO读写操作时,会向应用程序发出通知,应用程序直接获取数据即可。 同步说的是Server服务端的执行方式,阻塞说的是具体的技术,接收数据的方式、状态(io、nio)。 ## 2.nio的实现 通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据。通道与流的不同之处在于通道是双向的, Selector(选择器、多路复用器) 多路复用器提供选择已经就绪的任务的能力。Selector会不断的轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。一个多路复用器(Selector)可以负责成千上万的通道(Channel),没有上限。这也是JDK使用了epoll代替传统的select实现,获得连接句柄(客户端)没有限制。那也就意味着我们只要一个线程负责Selector的轮询,就可以接入成千上万个客户端,这是JDK NIO库的巨大进步。 Selector模式:当IO事件(管道)注册到选择器以后,Selector会分配给每个管道一个key值,相当于标签。Selector选择器是以轮询的方式进行查找注册的所有IO事件(管道),当IO事件(管道)准备就绪后,Selector就会识别,会通过key值来找到相应的管道,进行相关的数据处理操作(从管道中读取或写入数据,写到缓冲区中)。每个管道都会对选择器进行注册不同的事件状态,以便选择器查找。 ![img](http://cdn.processon.com/5e6b8fa7e4b01853041fd578?e=1584111031&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:HbbHVnmA_QxLzjoYTX0POjdxHrA=) ## 3.关于http协议 ### 1.http协议组成 1.请求行 2.请求头 3.空行 4.消息主体 ### 2.http协议请求方式 根据HTTP标准,HTTP请求可以使用多种请求方法。 HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。 HTTP1.1新增了五种请求方法: OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。 GET 请求指定的页面信息,并返回实体主体。 HEAD 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。(创建或更新) PUT 从客户端向服务器传送的数据取代指定的文档的内容。(更新) DELETE 请求服务器删除指定的页面。 CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。 OPTIONS 允许客户端查看服务器的性能。 TRACE 回显服务器收到的请求,主要用于测试或诊断。 ### 3.http状态码 状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别: 1xx:指示信息--表示请求已接收,继续处理 2xx:成功--表示请求已被成功接收、理解、接受 3xx:重定向--要完成请求必须进行更进一步的操作 4xx:客户端错误--请求有语法错误或请求无法实现 5xx:服务器端错误--服务器未能实现合法的请求 常见状态码: 200 OK //客户端请求成功 400 Bad Request //客户端请求有语法错误,不能被服务器所理解 401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用 403 Forbidden //服务器收到请求,但是拒绝提供服务 404 Not Found //请求资源不存在,eg:输入了错误的URL 500 Internal Server Error //服务器发生不可预期的错误 503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常 ## 4.https的原理 ### 1.HTTPS工作流程 ![img](http://cdn.processon.com/5e72e5ffe4b03b99652326a7?e=1584591887&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:DOSh7pik_nGTW5UXl9i8vQjuRic=) 1.Client发起一个HTTPS(比如https://juejin.im/user/5a9a9cdcf265da238b7d771c)的请求,根据RFC2818的规定,Client知道需要连接Server的443(默认)端口。 2.Server把事先配置好的公钥证书(public key certificate)返回给客户端。 3.Client验证公钥证书:比如是否在有效期内,证书的用途是不是匹配Client请求的站点,是不是在CRL吊销列表里面,它的上一级证书是否有效,这是一个递归的过程,直到验证到根证书(操作系统内置的Root证书或者Client内置的Root证书)。如果验证通过则继续,不通过则显示警告信息。 4.Client使用伪随机数生成器生成加密所使用的对称密钥,然后用证书的公钥加密这个对称密钥,发给Server。 5.Server使用自己的私钥(private key)解密这个消息,得到对称密钥。至此,Client和Server双方都持有了相同的对称密钥。 6.Server使用对称密钥加密“明文内容A”,发送给Client。 7.Client使用对称密钥解密响应的密文,得到“明文内容A”。 8.Client再次发起HTTPS的请求,使用对称密钥加密请求的“明文内容B”,然后Server使用对称密钥解密密文,得到“明文内容B” ### 2.数字证书认证机构的业务流程 服务器的运营人员向第三方机构CA提交公钥、组织信息、个人信息(域名)等信息并申请认证; CA通过线上、线下等多种手段验证申请者提供信息的真实性,如组织是否存在、企业是否合法,是否拥有域名的所有权等; 如信息审核通过,CA会向申请者签发认证文件-证书。证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA的信息、有效时间、证书序列号等信息的明文,同时包含一个签名。 其中签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用 CA的私钥对信息摘要进行加密,密文即签名; 客户端 Client 向服务器 Server 发出请求时,Server 返回证书文件; 客户端 Client 读取证书中的相关的明文信息,采用相同的散列函数计算得到信息摘要,然后,利用对应 CA的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性,即服务器的公开密钥是值得信赖的。 客户端还会验证证书相关的域名信息、有效时间等信息; 客户端会内置信任CA的证书信息(包含公钥),如果CA不被信任,则找不到对应 CA的证书,证书也会被判定非法。 ## 5.解释一下三次握手 四次挥手 https://blog.csdn.net/u014401141/article/details/104850002 为什么不能两次握手 TCP是一个双向通信协议,通信双方都有能力发送信息,并接收响应。如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认 为什么连接的时候是三次握手,关闭的时候却是四次握手 因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。 ## 6.Restful架构风格 ### 1.REST 风格 REST 是一种系统架构设计风格,主要面向基于网络的软件架构设计。这一架构风格,包含了以下一些基本要求: **客户-服务器** 在 REST 风格中,最基本的要求就是对于一个程序来说,应当分离用户接口和数据存储,改善用户接口跨平台迁移的可移植性,同时简化服务器组件,改善系统的可伸缩性。 对于这一点,目前我们所接触到的大多数应用都已经使用了这一模式,无论是浏览器,还是自主开发的客户端程序,其根本的实现方式都是采用了客户-服务器模式。客户端负责与用户之间的交互处理,而服务器端则实现数据存储以及相关的业务逻辑。 同时,对于服务器端,完整的系统大部分情况下都会包含多个不同的模块,这些模块之间的调用也应当遵循客户-服务器的模式,模块之间通过接口进行互相访问。 **无状态** 服务端在设计接口时,应当设计为无状态接口。也就是说,服务器端不保存任何与客户端相关的状态上下文信息,客户端在每次调用服务端接口时,需要提供足够的信息,以供服务端完成操作。 在无状态的设计中,服务端减少了保存客户端相关上下文数据,因此,一方面服务端能够更加容易的实现动态扩展,而不至于影响客户端使用;另一方面则减少了服务端从故障中恢复的任务量。 但无状态也会带来额外的问题。客户端将需要保存完整的用户状态信息,在每次与服务端交互时可能需要增加与用户状态相关的上下文信息,这样将导致请求数据的重复和增大。 **缓存** 根据接口的实际情况,应当在接口设计中增加缓存策略,服务端可以决定是否可以缓存当前返回的数据。通过此种方式,可以在一定程度上减少实际到达服务端请求,从而提高网络访问性能。 但缓存需要谨慎使用,缓存哪些数据,缓存过期时间都是需要根据实际情况进行设计。适当的缓存可以有效的提高系统效率,但是如果设计不当,将有可能导致大量的过期数据,进而影响系统运行。 一般而言,数据字典类数据、修改频率非常低的数据、实时性要求很低的数据等,这些数据可以设计一定的缓存策略,以提高系统运行效率。 **系统分层** 在设计系统,尤其是大型系统,通常需要将系统按照不同的功能进行横向和纵向的分层。例如横向分层一般可分为交互层、服务层、数据层等,而纵向分层则通常会按照不同的业务功能对系统进行切分。经过分层后,系统将划分为不同的模块进行独立开发部署运行。系统分层后,不同的模块可以独立进化,实现功能解耦,提高整个系统的可扩展性。 **统一接口** 统一接口,即是不同系统模块之间的调用接口统一规范,使用统一的调用协议,统一的数据格式等。统一接口带来的是系统交互的规范化,接口调用与业务解耦,各模块独立进化。 ### 2.主要原则 1.网络上的所有事物都被抽象为资源 2.每个资源都有一个唯一的资源标识符 3.同一个资源具有多种表现形式 4.对资源的各种操作不会改变资源标识符 5.所有的操作都是无状态的 6.符合REST原则的架构方式即可称为Restful ### 3.具体实现 面向资源-- 在 Restful 架构中,所有的接口应当采用面向资源的接口设计,即对于接口的访问地址指向其 URI 地址。 表述性---- 资源在网络上呈现出来的可能是多种形式,例如 HTML 、 XML 、 JSON 、图片等等。而客户端与服务器之间则传输的是资源的这种具体表现形式。客户端与服务端的互动,本质上就是通过这些表现形式,实现对资源的操作。 按照面向资源接口设计的要求,通常所见到的 URI 地址中,*.html / *.xml / *.json 等扩展名,其实都指向了当前资源的具体表现形式,而 URI 严格意义上仅指向了资源实体,并不包含具体表现形式。 状态转移---- 为了使操作资源,也即使资源发生状态转移,按照 REST 的要求,客户端若想要操作服务端资源,需要通过 HTTP 协议进行操作。而在 HTTP 协议中,规定了若干用于具体操作的动词,指向了不同的操作类型。 一般而言,对于资源的操作可以表示 CRUD 四类最基本的操作,即 增删改查 。而 HTTP 协议中的通常用以下动词表示这四类具体的操作: GET :查询资源操作。 POST :新建资源操作,也可以用于更新资源。 PUT :更新资源操作。 DELETE :删除资源操作。 在实际应用中,客户端与服务端之间的交互,即是建立在 HTTP 协议之上,通过面向资源的接口地址,使用 HTTP 协议动词作为操作描述,进而实现客户端与服务端的交互过程。 # 5.设计模式 ## 1.设计模式的七大原则 **1、开闭原则(Open Close Principle)** 开闭原则就是说**对扩展开放,对修改关闭**。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。 **2、里氏代换原则(Liskov Substitution Principle)** 里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科 **3、依赖倒转原则(Dependence Inversion Principle)** 这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。 **4、接口隔离原则(Interface Segregation Principle)** 这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件[架构](http://lib.csdn.net/base/architecture)出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。 **5、迪米特法则(最少知道原则)(Demeter Principle)** 为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 **6.单一职责原则** 定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。 问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。 解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。 **7、合成复用原则(Composite Reuse Principle)** 原则是尽量使用合成/聚合的方式,而不是使用继承。 ## 2.23种设计模式 **创建型模式,共五种**: 1.工厂方法模式。 2.抽象工厂模式。 3.单例模式。 4.建造者模式。 5.原型模式。 创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。 创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。 **结构型模式,共七种**: 1.适配器模式。 2.装饰器模式。 3.代理模式。 4.外观模式。 5.桥接模式。 6.组合模式。 7.享元模式。 结构型模式(Structural Pattern): 描述如何将类或者对象结合在一起形成更大的结构,就像搭积木,可以通过简单积木的组合形成复杂的、功能更为强大的结构 结构型模式(Structural Pattern) 结构型模式可以分为类结构型模式和对象结构型模式: 类结构型模式关心类的组合,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。 对象结构型模式关心类与对象的组合,通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是对象结构型模式。 记忆:享元代理外观装饰,桥接组合适配 **行为型模式** 共十一种: 1.策略模式 2.模板方法模式 3.观察者模式 4.迭代子模式 5.责任链模式 6.命令模式 7.备忘录模式 8.状态模式 9.访问者模式 10.中介者模式 11.解释器模式。 行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。 行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。 通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象之间的交互。在系统运行时,对象并不是孤立的,它们可以通过相互通信与协作完成某些复杂功能,一个对象在运行时也将影响到其他对象的运行。 行为型模式分为类行为型模式和对象行为型模式两种: 类行为型模式: 类的行为型模式使用继承关系在几个类之间分配行为,类行为型模式主要通过多态等方式来分配父类与子类的职责。 对象行为型模式: 对象的行为型模式则使用对象的聚合关联关系来分配行为,对象行为型模式主要是通过对象关联等方式来分配两个或多个类的职责。根据“合成复用原则”,系统中要尽量使用关联关系来取代继承关系,因此大部分行为型设计模式都属于对象行为型设计模式。 记忆: 1.中介者有责任命令解释器迭代状态 2.访问者解释模板 3.观察者备忘策略 ## 3.常用的设计模式 单例模式:https://blog.csdn.net/u014401141/article/details/63289833 代理模式:https://blog.csdn.net/u014401141/article/details/79017464 模板模式:https://blog.csdn.net/u014401141/article/details/53941049 责任链模式:https://blog.csdn.net/u014401141/article/details/70983753 在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。 委派模式:https://blog.csdn.net/u014401141/article/details/83650756 ## 4.面试问题 # 6.数据库 ## 6.1 Mysql ### 1 常见问题 **1.数据的隔离级别,分别解决了什么问题?**** **2.联合索引?怎么建索引?** **3.Mysql的锁机制,怎么加行锁,表锁?** **4.数据库优化思路?** **5.索引的数据结构,为什么用b+tree?** **6.掌握事务的特性与事务并发造成的问题** **7.事务读一致性问题的解决方案** **8.MVCC 的原理 ** **9. 锁的分类、行锁的原理、行锁的算法** 10. **死锁的产生** ### 2. MySQL架构与SQL执行流程 #### 1. 一条查询 SQL 语句是如何执行的执行流程 ![img](http://cdn.processon.com/5e7341fee4b03b9965249f7c?e=1584615438&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:VnYPAHpTRBOLLD-gepoNXDPAA4o=) **MYSQL架构** ![img](http://cdn.processon.com/5e734119e4b092510f653af8?e=1584615209&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:5z-flmwSp-0WSHzUI66mANiFG5Y=) 1、 Connector:用来支持各种语言和 SQL 的交互,比如 PHP,Python,Java 的 JDBC; 2、 Management Serveices & Utilities:系统管理和控制工具,包括备份恢复、MySQL 复制、集群等等; 3、 Connection Pool:连接池,管理需要缓冲的资源,包括用户密码权限线程等 等; 4、 SQL Interface:用来接收用户的 SQL 命令,返回用户需要的查询结果 5、 Parser:用来解析 SQL 语句; 6、 Optimizer:查询优化器; 7、 Cache and Buffer:查询缓存,除了行记录的缓存之外,还有表缓存,Key 缓 存,权限缓存等等; 8、 Pluggable Storage Engines:插件式存储引擎,它提供 API 给服务层使用, 跟具体的文件打交道。 **具体流程** **1、查询缓存** MySQL的缓存默认是关闭的,MySQL 8.0中,查询缓存已经被移除了 **2、解析器生成解析树** 主要做的事情是对语句基于SQL语法进行词法和语法分析和语义的解析 **3、预处理再次生成解析树** **4、查询优化器** 根据解析树生成不同的执行计划(ExecutionPlan),然后选择一种最优的执行计划,MySQL里面使用的是基于开销(cost)的优化器,那种执行计划开销最小,就用哪种。 优化器最终会把解析树变成一个查询执行计划,查询执行计划是一个数据结构。 Explain的结果也不一定最终执行的方式。 **6、查询执行引擎** 是谁使用执行计划去操作存储引擎呢? 这就是我们的执行引擎,它利用存储引擎提供的相应的API来完成操作。 **执行引擎** 1). 什么是存储引擎? 即:保存"数据"的形式【格式】 MYISAM:【处理快-相对不安全-不支持事务】 good.frm--说明书[声明表结构的表具体语句] good.MYD--数据内容 goods.MYI--目录[索引文件] 适合:只读之类的数据分析的项目。 InnoDB【安全-处理慢-支持事务】--只有.frm文件,其余表的其余全部内容存放在了一个文件中 特点: 支持事务,支持外键,因此数据的完整性、一致性更高。 支持行级别的锁和表级别的锁。 支持读写并发,写不阻塞读(MVCC)。 特殊的索引存放方式,可以减少IO,提升查询效率。 适合:经常更新的表,存在并发读写或者有事务处理的业务系统。 Memory【存放在内存中--一关机就没有了】 ![img](http://cdn.processon.com/5e761bdfe4b06b852fecd8fe?e=1584802287&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:LH4H4uzhfFcr37EvlLHG2LYd5WY=) **如何选择存储引擎** 如果对数据一致性要求比较高,需要事务支持,可以选择InnoDB 如果数据查询多更新少,对查询性能要求比较高,可以选择MyISAM 如果需要一个用于查询的临时表,可以选择Memory **7、查询数据返回结果** #### 2.一条更新 SQL 是如何执行的 与查询基本流程也是一致的,也就是说,它也要经过解析器、优化器的处理,最后交给执行器。 **区别就在于拿到符合条件的数据之后的操作** **缓冲池 Buffer Pool** 首先,InnnoDB 的数据都是放在磁盘上的,InnoDB 操作数据有一个最小的逻辑单位,叫做页(索引页和数据页)。我们对于数据的操作,不是每次都直接操作磁盘,因为磁盘的速度太慢了。InnoDB 使用了一种缓冲池的技术,也就是把磁盘读到的页放到一块内存区域里面。这个内存区域就叫 Buffer Pool 下一次读取相同的页,先判断是不是在缓冲池里面,如果是,就直接读取,不用再次访问磁盘。修改数据的时候,先修改缓冲池里面页。内存的数据页和磁盘数据不一致的时候,我们把它叫做脏页。InnoDB里面有专门的后台线程把Buffer Pool的数据写入到磁盘,每隔一段时间就一次性地把多个修改写入磁盘,这个动作就叫做刷脏。 Buffer Pool 主要分为 3 个部分: Buffer Pool、Change Buffer、Adaptive Hash Index,另外还有一个(redo)log buffer。 **redo Log Buffer** 如果BufferPool里面的脏页还没有刷入磁盘时,数据库宕机或者重启,这些数据丢失。如果写操作写到一半,甚至可能会破坏数据文件导致数据库不可用。 为了避免这个问题, InnoDB把所有对页面的修改操作专门写入一个日志文件,并且在数据库启动时从这个文件进行恢复操作(实现crash-safe)——用它来实现事务的持久性。这个文件就是磁盘的redo log(叫做重做日志),对应于/var/lib/mysql/目录下的ib_logfile0和ib_logfile1,每个48M。 redo log的内容主要是用于崩溃恢复。磁盘的数据文件,数据来自buffer pool。 redo log写入磁盘,不是写入数据文件。 那么,Log Buffer什么时候写入log file? 在我们写入数据到磁盘的时候,操作系统本身是有缓存的。flush就是把操作系统缓冲区写入到磁盘。 **undo log** (撤销日志或回滚日志)记录了事务发生之前的数据状态(不包括select) 。 如果修改数据时出现异常,可以用undo log来实现回滚操作(保持原子性)。 **bin log** bin log以事件的形式记录了所有的DDL和DML语句(因为它记录的是操作而不是 数据值,属于逻辑日志),可以用来做主从复制和数据恢复 在开启了binlog功能的情况下,我们可以把binlog导出成SQL语句,把所有的操作重放一遍,来实现数据的恢复。 binlog的另一个功能就是用来实现主从复制,它的原理就是从服务器读取主服务器的binlog,然后执行一遍。 ### 3. MySQL事务与锁详解 #### 1.事务 ##### 1.1 事务的四大特性 1、原子性:数据库事务不可分割的单位,要么都做,要么都不做。 实现 在 InnoDB 里面是通过 undo log 来实现的,它记录了数据修改之前的值(逻辑日志),一旦发生异常,就可以用 undo log 来实现回滚操作。 2、一致性:事务的操作不会改变数据库的状态,比方说唯一约束 3、隔离性:事务是相互不可见的 4、持久性:事务一旦提交,即使宕机也是能恢复的 持久性是通过 redo log 和 double write 双写缓冲来实现的,我们操作数据的时候, 会先写到内存的 buffer pool 里面,同时记录 redo log,如果在刷盘之前出现异常,在重启后就可以读取 redo log 的内容,写入到磁盘,保证数据的持久性。 当然,恢复成功的前提是数据页本身没有被破坏,是完整的,这个通过双写缓冲(double write)保证。 ##### 1.2 事务问题 1.脏读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新,然后B回滚操作,那么A读取到的数据是脏数据。 2.不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。 3.幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。 无论是脏读,还是不可重复读,还是幻读,它们都是数据库的读一致性的问题,都是在一个事务里面前后两次读取出现了不一致的情况。 不可重复读和幻读的区别在那里呢? 不可重复读是修改或者删除,幻读是插入 ##### 1.3 数据库什么时候会出现事务 **自动开启** `update student set name='猫老公' where id=1;` 实际上,它自动开启了一个事务,并且提交了,所以最终写入了磁盘。 这个是开启事务的第一种方式,自动开启和自动提交。 **手动开启事务** 手动开启事务也有几种方式,一种是用begin;一种是用start transaction。 还有一种情况,客户端的连接断开的时候,事务也会结束。 ##### 1.4 事务隔离级别 ![](https://i.loli.net/2020/11/11/53JLHtE12dujZFP.png) 1.**读未提交(READ UNCOMMITTED)**:未提交读隔离级别也叫读脏,就是事务可以读取其它事务未提交的数据。 2.**读已提交(READ COMMITTED)**:在其它数据库系统比如 SQL Server 默认的隔离级别就是提交读,已提交读 隔离级别就是在事务未提交之前所做的修改其它事务是不可见的。 3.**可重复读(REPEATABLE READ)**:保证同一个事务中的多次相同的查询的结果是一致的,比如一个事务一开始 查询了一条记录然后过了几秒钟又执行了相同的查询,保证两次查询的结果是相同的,可重复读也是 mysql 的默认隔 离级别。 4.**可串行化(SERIALIZABLE)**:可串行化就是保证读取的范围内没有新的数据插入,比如事务第一次查询得到某个 范围的数据,第二次查询也同样得到了相同范围的数据,中间没有新的数据插入到该范围中。 ##### 1.5 MySQL InnoDB 对隔离级别的支持 ###### 1.5.1 基于锁的并发控制 Lock Based Concurrency Control(LBCC) LBCC 第一种,我既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要操作的数据,不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制 Lock Based Concurrency Control(LBCC)。 如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地 影响操作数据的效率。 ###### 1.5.2 多版本的并发控制 Multi Version Concurrency Control (MVCC) 让一个事务前后两次读取的数据保持一致, 那么我们可以在修改数据的时候给它建立一个备份或者叫快照,后面再来读取这个快照就行了。这种方案我们叫做多版本的并发控制 Multi Version Concurrency Control (MVCC)。 MVCC 的核心思想是: 我可以查到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。 InnoDB 为每行记录都实现了两个隐藏字段: DB_TRX_ID,6 字节:插入或更新行的最后一个事务的事务 ID,事务编号是自动递 增的(我们把它理解为创建版本号,在数据新增或者修改为新数据的时候,记录当前事 务 ID)。 DB_ROLL_PTR,7 字节:回滚指针(我们把它理解为删除版本号,数据被删除或记 录为旧数据的时候,记录当前事务 ID)。 **MVCC 的查找规则:只能查找创建时间小于等于当前事务 ID 的数据,和删除时间大 于当前事务 ID 的行(或未删除)。** 在 InnoDB 中,MVCC 是通过 Undo log 实现的。 Oracle、Postgres 等等其他数据库都有 MVCC 的实现。 需要注意,在 InnoDB 中,MVCC 和锁是协同使用的,这两种方案并不是互斥的。 第一大类解决方案是锁,锁又是怎么实现读一致性的呢? #### 2. InnoDB锁 ##### 2.1 MySQL InnoDB 锁的基本类型 **共享锁(s-行锁):又称读锁** 第一个行级别的锁就是我们在官网看到的 Shared Locks (共享锁),我们获取了 一行数据的读锁以后,可以用来读取数据,所以它也叫做读锁。 允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。 我们可以用select …… lock in share mode; 的方式手工加上一把读锁。 释放锁有两种方式,只要事务结束,锁就会自动事务,包括提交事务和结束事务。 **排他锁(X-行锁):又称写锁** 它是用来操作数据的,所以又 叫做写锁。只要一个事务获取了一行数据的排它锁,其他的事务就不能再获取这一行数 据的共享锁和排它锁。 排它锁的加锁方式有两种,第一种是自动加排他锁。我们在操作数据的时候,包括 增删改,都会默认加上一个排它锁。 还有一种是手工加锁,我们用一个 FOR UPDATE 给一行数据加上一个排它锁,这个 无论是在我们的代码里面还是操作数据的工具里面,都比较常用。 **意向锁(意向锁都是表锁)** 当我们给一行数据加上共享锁之前,数据库会自动在这张表上面加一个 意向共享锁。 当我们给一行数据加上排他锁之前,数据库会自动在这张表上面加一个意向排他锁。 反过来说: 如果一张表上面至少有一个意向共享锁,说明有其他的事务给其中的某些数据行加 上了共享锁。 如果一张表上面至少有一个意向排他锁,说明有其他的事务给其中的某些数据行加 上了排他锁。 那么这两个表级别的锁存在的意义是什么呢?第一个,我们有了表级别的锁,在 InnoDB 里面就可以支持更多粒度的锁。它的第二个作用,我们想一下,如果说没有意向 锁的话,当我们准备给一张表加上表锁的时候,我们首先要做什么?是不是必须先要去 判断有没其他的事务锁定了其中了某些行?如果有的话,肯定不能加上表锁。那么这个 时候我们就要去扫描整张表才能确定能不能成功加上一个表锁,如果数据量特别大,比 如有上千万的数据的时候,加表锁的效率是不是很低? 但是我们引入了意向锁之后就不一样了。我只要判断这张表上面有没有意向锁,如 果有,就直接返回失败。如果没有,就可以加锁成功。所以 InnoDB 里面的表锁,我们 可以把它理解成一个标志。就像火车上厕所有没有人使用的灯,是用来提高加锁的效率的。 ##### 2.2 行锁的原理 InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。 1、为什么表里面没有索引的时候,锁住一行数据会导致锁表?或者说,如果锁住的是索引,一张表没有索引怎么办? 所以,一张表有没有可能没有索引? 1)如果我们定义了主键(PRIMARYKEY),那么 InnoDB 会选择主键作为聚集索引。 2)如果没有显式定义主键,则 InnoDB 会选择第一个不包含有 NULL 值的唯一索引作为主键索引。 3)如果也没有这样的唯一索引,则 InnoDB 会选择内置 6 字节长的 ROWID 作为隐藏的聚集索引,它会随着行记录的写入而主键递增。所以,为什么锁表,是因为查询没有使用索引,会进行全表扫描,然后把每一个隐藏的聚集索引都锁住了。 ##### 2.3 锁的算法 **记录锁** 当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,精准匹配到一条记录的时候,这个时候使用的就是记录锁。 **间隙锁** 当我们查询的记录不存在,没有命中任何一个record,无论是用等值查询还是范围查询的时候,它使用的都是间隙锁。 重复一遍,当查询的记录不存在的时候,使用间隙锁。 注意,间隙锁主要是阻塞插入insert。相同的间隙锁之间不冲突。 **临键锁** 当我们使用了范围查询,不仅仅命中了Record记录,还包含了 Gap间隙,在这种情况下我们使用的就是临键锁,它是MySQL里面默认的行锁算法,相当于记录锁加上间隙锁。 其他两种退化的情况: 唯一性索引,等值查询匹配到一条记录的时候,退化成记录锁。 没有匹配到任何记录的时候,退化成间隙锁。 为什么要锁住下一个左开右闭的区间?——就是为了解决幻读的问题。 **四个事务隔离级别的实现** **Read Uncommited** RU隔离级别:不加锁。 **Serializable** Serializable 所有的 select 语句都会被隐式的转化为select ... in share mode,会和update、delete互斥。 这两个很好理解,主要是RR和RC的区别? **Repeatable Read** RR隔离级别下,普通的select使用快照读(snapshotread),底层使用MVCC来实现。 加锁的 select(select ... in share mode / select ... for update)以及更新操作update, delete 等语句使用当前读(current read),底层使用记录锁、或者间隙锁、临键锁。 **Read Commited** RC隔离级别下,普通的select都是快照读,使用MVCC实现。加锁的select都使用记录锁,因为没有Gap Lock。 除了两种特殊情况——外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会使用间隙锁封锁区间。所以RC会出现幻读的问题。 ##### 2.4 死锁 **死锁的产生条件** ![](https://i.loli.net/2020/11/11/zJrmUCW4gf1tdPk.png) (1)同一时刻只能有一个事务持有这把锁 (2)其他的事务需要在这个事务释放锁之后才能获取锁,而不可以强行剥夺 (3)当多个事务形成等待环路的时候,即发生死锁。 **死锁的避免** 1、 在程序中,操作多张表时,尽量以相同的顺序来访问(避免形成等待环路); 2、 批量操作单张表数据的时候,先对数据进行排序(避免形成等待环路); 3、 申请足够级别的锁,如果要操作数据,就申请排它锁; 4、 尽量使用索引访问数据,避免没有where条件的操作,避免锁表; 5、 如果可以,大事务化成小事务; 6、 使用等值查询而不是范围查询查询数据,命中记录,避免间隙锁对并发的影响。 **查看锁信息(日志)** SHOW STATUS 命令中,包括了一些行锁的信息: `show status like 'innodb_row_lock_%';` 非实时 Innodb_row_lock_current_waits:当前正在等待锁定的数量; Innodb_row_lock_time :从系统启动到现在锁定的总时间长度,单位 ms; Innodb_row_lock_time_avg :每次等待所花平均时间; Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间; Innodb_row_lock_waits :从系统启动到现在总共等待的次数。 SHOW 命令是一个概要信息。InnoDB 还提供了三张表来分析事务与锁的情况: `select * from information_schema.INNODB_TRX; -- 当前运行的所有事务 ,还有具体的语句` `select * from information_schema.INNODB_LOCKS; -- 当前出现的锁` `select * from information_schema.INNODB_LOCK_WAITS; -- 锁等待的对应关系` ### 4. MySQL索引深入剖析 #### 1.基本问题 **1. 理解索引的本质** **2. 通过推演掌握索引底层的数据结构** **3.掌握在不同存储引擎中索引的落地方式** **4. 掌握索引的创建和使用个原则** **5.联合索引** **6.B+Tree** #### 2.索引类型 1.Normal:普通的索引;允许一个索引值后面关联多个行值; 2.UNIQUE:唯一索引;允许一个索引值后面只能有一个行值;之前对列添加唯一约束其实就是为这列添加了一个unique索引;当我们为一个表添加一个主键的时候,其实就是为这个表主键列(设置了非空约束),并为主键列添加了一个唯一索引; 3.Fulltext:全文检索,mysql的全文检索只能用myisam引擎,并且性能较低,不建议使用; #### 3.实现 1.b-tree:是一颗树(二叉树,平衡二叉树,平衡树(B-TREE)) 使用平衡树实现索引,是mysql中使用最多的索引类型;在innodb中,存在两种索引类型。 第一种是主键索引(primary key),在索引内容中直接保存数据的地址; 第二种是其他索引,在索引内容中保存的是指向主键索引的引用;所以在使用innodb的时候,要尽量的使用主键索引,速度非常快; 1、它的关键字的数量是跟路数相等的; 2、B+Tree的根节点和枝节点中都不会存储数据,只有叶子节点才存储数据。搜索到关键字不会直接返回,会到最后一层的叶子节点。比如我们搜索 id=28,虽然在第一层直接命中了,但是全部的数据在叶子节点上面,所以我还要继续往下搜索,一直到叶子节点。 3、B+Tree的每个叶子节点增加了一个指向相邻叶子节点的指针,它的最后一个数据会指向下一个叶子节点的第一个数据,形成了一个有序链表的结构。 4、它是根据左闭右开的区间 [ )来检索数据。 总结一下,InnoDB中的B+Tree的特点: 1)它是BTree的变种,BTree能解决的问题,它都能解决。BTree解决的两大问题是什么?(每个节点存储更多关键字;路数更多) 2)扫库、扫表能力更强(如果我们要对表进行全表扫描,只需要遍历叶子节点就可以了,不需要遍历整棵B+Tree拿到所有的数据) 3)B+Tree的磁盘读写能力相对于BTree来说更强(根节点和枝节点不保存数据区,所以一个节点可以保存更多的关键字,一次磁盘加载的关键字更多) 4)排序能力更强(因为叶子节点上有下一个数据区的指针,数据形成了链表) 5)效率更加稳定(B+Tree永远是在叶子节点拿到数据,所以IO次数是稳定的) ![img](http://cdn.processon.com/5fabb664f346fb1859f52649?e=1605092468&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:nrjhMIDcKDAcTW-v8_N57bVSeBA=) 为什么不用红黑树?1、只有两路;2、不够平衡。 总结一下,InnoDB 中的 B+Tree 的特点: 2.Hash:把索引的值做hash运算,并存放到hash表中,使用较少,一般是memory引擎使用;优点:因为使用hash表存储,按照常理,hash的性能比B-TREE效率高很多。 hash索引的缺点: 1,hash索引只能适用于精确的值比较,=,in,或者<>;无法使用范围查询; 2,无法使用索引排序; 3,组合hash索引无法使用部分索引; 4,如果大量索引hash值相同,性能较低; #### **4.聚集索引** 什么叫做聚集索引(聚簇索引)? 就是索引键值的逻辑顺序跟表数据行的物理存储顺序是一致的。(比如字典的目录 是按拼音排序的,内容也是按拼音排序的,按拼音排序的这种目录就叫聚集索引)。 在 InnoDB 里面,它组织数据的方式叫做叫做(聚集)索引组织表(clustered index organize table),所以主键索引是聚集索引,非主键都是非聚集索引。 ![](https://i.loli.net/2020/11/12/bdRfhDIkuKeEg5i.png) InnoDB 中,主键索引和辅助索引是有一个主次之分的。 辅助索引存储的是辅助索引和主键值。如果使用辅助索引查询,会根据主键值在主 键索引中查询,最终取得数据。 比如我们用 name 索引查询 name= '青山',它会在叶子节点找到主键值,也就是 id=1,然后再到主键索引的叶子节点拿到数据。 ![](https://i.loli.net/2020/11/12/eD2kGZSJ3wP1IBW.png) 为什么在辅助索引里面存储的是主键值而不是主键的磁盘地址呢?如果主键的数据类型比较大,是不是比存地址更消耗空间呢? 我们前面说到 B Tree 是怎么实现一个节点存储多个关键字,还保持平衡的呢? 是因为有分叉和合并的操作,这个时候键值的地址会发生变化,所以在辅助索引里 面不能存储地址。 另一个问题,如果一张表没有主键怎么办? 1、如果我们定义了主键(PRIMARY KEY),那么 InnoDB 会选择主键作为聚集索引。 2、如果没有显式定义主键,则 InnoDB 会选择第一个不包含有 NULL 值的唯一索引作为主键索引。 3、如果也没有这样的唯一索引,则 InnoDB 会选择内置 6 字节长的 ROWID 作为隐 藏的聚集索引,它会随着行记录的写入而主键递增。 #### 5.索引使用原则 ##### 5.1 索引使用原则 **列的离散(sàn)度** 第一个叫做列的离散度,我们先来看一下列的离散度的公式: count(distinct(column_name)) : count(*),列的全部不同值和所有数据行的比例。 数据行数相同的情况下,分子越大,列的离散度就越高。 建立索引,要使用离散度(选择度)更高的字段。如果在 B+Tree 里面的重复值太多,MySQL 的优化器发现走索引跟使用全表扫描差 不了多少的时候,就算建了索引,也不一定会走索引。 ##### 5.2 索引的创建与使用 1、在用于where判断order排序和join的(on)字段上创建索引。 2、索引的个数不要过多。 ——浪费空间,更新变慢。 3、区分度低的字段,例如性别,不要建索引。 ——离散度太低,导致扫描行数过多。 4、频繁更新的值,不要作为主键或者索引。 ——页分裂 5、组合索引把散列性高(区分度高)的值放在前面。 6、创建复合索引,而不是修改单列索引。 ##### 5.3 **索引失效的条件** - 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描 - 存储引擎不能使用索引**范围条件**右边的列 - 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select * - mysql在使用不等于(!=或者<>)的时候无法使用索引会导致全表扫描 - is null,is not null也无法使用索引 ***---- 此处存在疑问,经测试确实可以使用,ref和const等级,并不是all*** - like以通配符开头(’%abc…’)mysql索引失效会变成全表扫描的操作。问题:解决like‘%字符串%’时索引不被使用的方法? - 字符串不加单引号索引失效 **mysql 联合索引生效的条件、索引失效的条件** https://blog.csdn.net/u014401141/article/details/107855776 ### 5. MySQL性能优化总结 #### 1.基本问题 掌握 MySQL 数据库优化的层次和思路 掌握 MySQL 数据库优化的工具。 #### 2.优化思路 作为架构师或者开发人员,说到数据库性能优化,你的思路是什么样的? 我们说到性能调优,大部分时候想要实现的目标是让我们的查询更快。一个查询的 动作又是由很多个环节组成的,每个环节都会消耗时间,我们要减少查询所消耗的时间,就要从每一个环节入手。 ![img](http://cdn.processon.com/5facd312f346fb4abf984b4c?e=1605165347&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:WuN98P625ldD-iTj7O2lS-1tPVU=) ##### 1. 连接——配置优化 第一个环节是客户端连接到服务端,有可能是服务端连接数不够导致应用程序获取不到连接。 我们可以从两个方面来解决连接数不够的问题: 1、从服务端来说,我们可以增加服务端的可用连接数。 如果有多个应用或者很多请求同时访问数据库,连接数不够的时候,我们可以: (1)修改配置参数增加可用连接数,修改 max_connections 的大小: show variables like 'max_connections'; -- 修改最大连接数,当有多个应用连接的时候 (2)或者,或者及时释放不活动的连接。交互式和非交互式的客户端的默认超时时 间都是 28800 秒,8 小时,我们可以把这个值调小。 show global variables like 'wait_timeout'; --及时释放不活动的连接,注意不要释放连接池还在使用的连接 2、从客户端来说,可以减少从服务端获取的连接数,我们可以引入连接池,实现连接的重用。 我们可以在哪些层面使用连接池? ORM 层面(MyBatis 自带了一个连接池);或者 使用专用的连接池工具(阿里的 Druid、Spring Boot 2.x 版本默认的连接池 Hikari、老 牌的 DBCP 和 C3P0)。 连接池大小设置: 连接池并不是越大越好,只要维护一定数量大小的连接池, 其他的客户端排队等待获取连接就可以了。有的时候连接池越大,效率反而越低。 Druid 的默认最大连接池大小是 8。Hikari 的默认最大连接池大小是 10。 为什么默认值都是这么小呢? 在 Hikari 的 github 文档中,给出了一个 PostgreSQL 数据库建议的设置连接池大小 的公式: https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing ,**它的建议是机器核数乘以 2 加 1**。也就是说,4 核的机器,连接池维护 9 个连接就 够了。这个公式从一定程度上来说对其他数据库也是适用的。 ##### 2. 缓存——架构优化 在应用系统的并发数非常大的情况下,如果没有缓存,会造成两个问题: 一方面是 会给数据库带来很大的压力。 另一方面,从应用的层面来说,操作数据的速度也会受到 影响。 我们可以用第三方的缓存服务来解决这个问题,例如 Redis。 运行独立的缓存服务,属于架构层面的优化. ##### 3.分库分表 垂直分库,减少并发压力。 水平分表,解决存储瓶颈。 垂直分库的做法,把一个数据库按照业务拆分成不同的数据库: ![](https://i.loli.net/2020/11/12/P3UGY4CW7wfsM2S.png) 水平分库分表的做法,把单张表的数据按照一定的规则分布到多个数据库。 ![](https://i.loli.net/2020/11/12/qozwQZFPRTAOhfi.png) ##### 4.优化器——SQL 语句分析与优化 优化器就是对我们的 SQL 语句进行分析,生成执行计划。 ###### 1. 慢查询日志 slow query log 打开慢日志开关 因为开启慢查询日志是有代价的(跟 bin log、optimizer-trace 一样),所以它默 认是关闭的: `show variables like 'slow_query%'; 除了这个开关,还有一个参数,控制执行超过多长时间的 SQL 才记录到慢日志,默 认是 10 秒。` `show variables like '%slow_query%'; 可以直接动态修改参数(重启后失效)。` `set @@global.slow_query_log=1; -- 1 开启,0 关闭,重启后失效` `set @@global.long_query_time=3; -- mysql 默认的慢查询时间是 10 秒,另开一个窗口后才会查到最新值` `show variables like '%long_query%';` ###### 2.慢日志分析 1. 日志内容 `show global status like 'slow_queries'; -- 查看有多少慢查询` `show variables like '%slow_query%'; -- 获取慢日志目录` 2. SHOW PROFILE https://dev.mysql.com/doc/refman/5.7/en/show-profile.html SHOW PROFILE可以查看 SQL 语句执行的时候使用的资源,比如 CPU、IO 的消耗情况。 在 SQL 中输入 help profile 可以得到详细的帮助信息。 查看是否开启 `select @@profiling; set @@profiling=1;` 查看 profile 统计 (命令最后带一个 s) show profiles; ![](https://i.loli.net/2020/11/12/ULPQDzOeKuyFwja.png) 查看最后一个 SQL 的执行详细信息,从中找出耗时较多的环节(没有 s)。 show profile; ![](https://i.loli.net/2020/11/12/Bs1WHCj7oGlgrpv.png) 6.2E-5,小数点左移 5 位,代表 0.000062 秒。 也可以根据 ID 查看执行详细信息,在后面带上 for query + ID。 `show profile for query 1;` 除了慢日志和 show profile,如果要分析出当前数据库中执行的慢的 SQL,还可以 通过查看运行线程状态和服务器运行信息、存储引擎信息来分析。 ###### 3.EXPLAIN执行计划 官方链接:https://dev.mysql.com/doc/refman/5.7/en/explain-output.html 我们先创建三张表。一张课程表,一张老师表,一张老师联系方式表(没有任何索引)。 ![](https://i.loli.net/2020/11/12/fxakw9X1eF7RKAM.png) ![](https://i.loli.net/2020/11/12/z3i8tSQhAJKLWca.png) explain 的结果有很多的字段,我们详细地分析一下。先确认一下环境: ![](https://i.loli.net/2020/11/12/JZQjIKmLinHsM7p.png) * id id 是查询序列编号。 id 值不同 id 值不同的时候,先查询id 值大的(先大后小)。 ![](https://i.loli.net/2020/11/12/jSkKNbhpWJ3UDvs.png) ![](https://i.loli.net/2020/11/12/QbwUXMJrcmdzioC.png) 查询顺序:course c——teacher t——teacher_contact tc。 先查课程表,再查老师表,最后查老师联系方式表。子查询只能以这种方式进行, 只有拿到内层的结果之后才能进行外层的查询。 id 值相同 ![](https://i.loli.net/2020/11/12/N9yxpRfArCnI62l.png) ![](https://i.loli.net/2020/11/12/VxYElNHAyFwhpGi.png) id 值相同时,表的查询顺序是从上往下顺序执行。例如这次查询的id 都是 1,查询的顺序是teacher t(3 条)——course c(4 条)——teacher_contact tc(3 条)(小标驱动大表的思想) 既有相同也有不同 如果ID 有相同也有不同,就是ID 不同的先大后小,ID 相同的从上往下。 **select_type查询类型** 这里并没有列举全部(其它:DEPENDENT UNION、DEPENDENT SUBQUERY、MATERIALIZED、UNCACHEABLE SUBQUERY、UNCACHEABLE UNION)。 下面列举了一些常见的查询类型: **SIMPLE** 简单查询,不包含子查询,不包含关联查询union。 ![](https://i.loli.net/2020/11/12/yaW7MpDBPUxnoH8.png) **PRIMARY** 子查询SQL 语句中的主查询,也就是最外面的那层查询。 **SUBQUERY** 子查询中所有的内层查询都是SUBQUERY 类型的。 **DERIVED** 衍生查询,表示在得到最终查询结果之前会用到临时表。例如: ![](https://i.loli.net/2020/11/12/TjIhCQno96uO5dL.png) ![](https://i.loli.net/2020/11/12/AREZH2kvaiUrnsY.png) 对于关联查询,先执行右边的 table(UNION),再执行左边的 table,类型是DERIVED。 **UNION** 用到了UNION 查询。同上例。 **UNION RESULT** 主要是显示哪些表之间存在 UNION 查询。代表 id=2 和 id=3 的查询存在UNION。同上例。 **type**连接类型 [https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain-join-types](https://dev.mysql.com/doc/refman/5.7/en/explain-output.html) 所有的连接类型中,上面的最好,越往下越差。 在常用的链接类型中:system > const > eq_ref > ref > range > index > all 这 里 并 没 有 列 举 全 部 ( 其 他 : fulltext 、 ref_or_null 、 index_merger 、unique_subquery、index_subquery)。 以上访问类型除了all,都能用到索引。 **const** 主键索引或者唯一索引,只能查到一条数据的SQL。 ![](https://i.loli.net/2020/11/12/8OhK3y2YG5LEfu6.png) ![](https://i.loli.net/2020/11/12/3aTJkvjDHWm98l6.png) **system** system 是const 的一种特例,只有一行满足条件。例如:只有一条数据的系统表。 ![](https://i.loli.net/2020/11/12/FGdWce28uZmBKfT.png) ![](https://i.loli.net/2020/11/12/vVLRQsp9IP5O3Z2.png) **eq_ref** 通常出现在多表的 join 查询,表示对于前表的每一个结果,,都只能匹配到后表的一行结果。一般是唯一性索引的查询(UNIQUE 或PRIMARY KEY)。 eq_ref 是除const 之外最好的访问类型。 先删除 teacher 表中多余的数据,teacher_contact 有 3 条数据,teacher 表有 3 条数据。 小结: 以上三种 system,const,eq_ref,都是可遇而不可求的,基本上很难优化到这个状态。 **ref** 查询用到了非唯一性索引,或者关联操作只使用了索引的最左前缀。例如:使用tcid 上的普通索引查询: ![](https://i.loli.net/2020/11/12/O2GsRvjDuxAlXUT.png) ![](https://i.loli.net/2020/11/12/RHkriSA65Lh91PJ.png) **range** 索引范围扫描。 如果where 后面是 between and 或 <或 > 或 >= 或 <=或in 这些,type 类型就为range。不走索引一定是全表扫描(ALL),所以先加上普通索引。IN 查询也是range(字段有主键索引) **index** Full Index Scan,查询全部索引中的数据(比不走索引要快)。 ![](https://i.loli.net/2020/11/12/tC87NQL4DouaYlb.png) ![](https://i.loli.net/2020/11/12/bYa4BIXtPlkmhng.png) **all** Full Table Scan,如果没有索引或者没有用到索引,type 就是ALL。代表全表扫描。 **NULL** 不用访问表或者索引就能得到结果,例如: ![](https://i.loli.net/2020/11/12/zBNqRhrlcbVOQm3.png) 小结: 一般来说,需要保证查询至少达到range 级别,最好能达到ref。ALL(全表扫描)和index(查询全部索引)都是需要优化的。 **possible_key、key** 可能用到的索引和实际用到的索引。如果是NULL 就代表没有用到索引。possible_key 可以有一个或者多个,可能用到索引不代表一定用到索引。 **key_len** 索引的长度(使用的字节数)。跟索引字段的类型、长度有关。 **rows** MySQL 认为扫描多少行才能返回请求的数据,是一个预估值。一般来说行数越少越好。 **filtered** 这个字段表示存储引擎返回的数据在server 层过滤后,剩下多少满足查询的记录数量的比例,它是一个百分比。 **ref** 使用哪个列或者常数和索引一起从表中筛选数据。 **Extra** 执行计划给出的额外的信息说明。 **using index** 用到了覆盖索引,不需要回表。 ![](https://i.loli.net/2020/11/12/7xMTtUDk6sGdyRW.png) **using where** 使用了where 过滤,表示存储引擎返回的记录并不是所有的都满足查询条件,需要在server 层进行过滤(跟是否使用索引没有关系)。 **Using index condition(索引条件下推)** 索引下推 **using filesort** 不能使用索引来排序,用到了额外的排序(跟磁盘或文件没有关系)。需要优化。 (复合索引的前提) **using temporary** 用到了临时表。例如(以下不是全部的情况): 1、distinct 非索引列 总结一下: 模拟优化器执行SQL 查询语句的过程,来知道 MySQL 是怎么处理一条SQL 语句的。通过这种方式我们可以分析语句或者表的性能瓶颈。 分析出问题之后,就是对SQL 语句的具体优化。 比如怎么用到索引,怎么减少锁的阻塞等待,在前面两次课已经讲过。 ###### 4.SQL与索引优化 1. 永远用小结果集驱动大的结果集 2. 在索引中完成排序 3. 使用最小Columns 4. 使用最有效的过滤条件 5. 避免复杂的JOIN和子查询 6. 尽量避免大事务操作,提高系统并发能力。 7. 尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。 8. 不在数据库做计算,cpu计算务必移至业务层 9. 控制单表数据量,单表记录控制在千万级 10. 控制列数量,字段数控制在20以内 11. 平衡范式与冗余,为提高效率可以牺牲范式设计,冗余数据 12. 拒绝3B(big),大sql,大事务,大批量 13. 避免对索引字段进行计算操作 14. 避免在索引字段上使用not,<>,!=,使用IS NULL和IS NOT NULL,数据类型转换,使用函数 15. 避免建立索引的列中使用空值。 16. 应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描in 和 not in 也要慎用,否则会导致全表扫描 17. 对于连续的数值,能用 between 就不要用 in 了,很多时候用 exists 代替 in 是一个好的选择。 18. 不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。 19. 不要以字符格式声明数字,要以数字格式声明字符值。(日期同样)否则会使索引无效,产生全表扫描。 ### 6.范式 #### 第一范式 即表的列的具有原子性,不可再分解,即列的信息,不能分解,只有数据库是关系型数据库(mysql/oracle/db2/informix/sysbase/sql server),就自动的满足1NF #### 第二范式 表中的记录是唯一的,就满足2NF,通常我们设计一个主键来实现 #### 第三范式 即表中不要有冗余数据,就是说,表的信息,如果能够被推导出来,就不应该单独的设计一个字段来存放. ### 7.数据库设计 https://blog.csdn.net/u014401141/article/details/78918810 # 7.框架 ## 7.1 Spring ### 1.Spring 的IOC和AOP IoC IoC(Inverse of Control:控制反转)是⼀种设计思想,就是 将原本在程序中⼿动创建对象的控制权,交由**Spring**框架来管理。 IoC 在其他语⾔中也有应⽤,并⾮ Spring 特有。 **IoC** 容器是 **Spring**⽤来实现 **IoC** 的载体, **IoC** 容器实际上就是个**Map**(**key**,**value**)**,Map** 中存放的是各种对象。将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注⼊。这样可以很⼤程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。 **IoC** 容器就像是⼀个⼯⼚⼀样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件**/**注解即可,完全不⽤考虑对象是如何被创建出来的。 在实际项⽬中⼀个 Service 类可能有⼏百甚⾄上千个类作为它的底层,假如我们需要实例化这个Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把⼈逼疯。如果利⽤IoC 的话,你只需要配置好,然后在需要的地⽅引⽤就⾏了,这⼤⼤增加了项⽬的可维护性且降低了开发难度。 Spring 时代我们⼀般通过 XML ⽂件来配置 Bean,后来开发⼈员觉得 XML ⽂件来配置不太好,于是SpringBoot 注解配置就慢慢开始流⾏起来。 **Spring IoC**的初始化过程: ![](https://i.loli.net/2021/01/07/QleBmTUnIkzWKhb.png) AOP AOP(Aspect-Oriented Programming:⾯向切⾯编程)能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。 **Spring AOP**就是基于动态代理的,如果要代理的对象,实现了某个接⼝,那么Spring AOP会使⽤JDK Proxy,去创建代理对象,⽽对于没有实现接⼝的对象,就⽆法使⽤ JDK Proxy 去进⾏代理了,这时候Spring AOP会使⽤**Cglib** ,这时候Spring AOP会使⽤ **Cglib** ⽣成⼀个被代理对象的⼦类来作为代理, 如下图所示: ![](https://i.loli.net/2021/01/07/efIrALJXpUY5nWt.png) 当然你也可以使⽤ AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java ⽣态系统中最完整的 AOP 框架了。 使⽤ AOP 之后我们可以把⼀些通⽤功能抽象出来,在需要⽤到的地⽅直接使⽤即可,这样⼤⼤简化了代码量。我们需要增加新功能时也⽅便,这样也提⾼了系统扩展性。⽇志功能、事务管理等等场景都⽤到了 AOP 。 Spring AOP 和 AspectJ AOP 有什么区别? **Spring AOP** 属于运⾏时增强,⽽ **AspectJ** 是编译时增强。 Spring AOP 基于代理(Proxying),⽽AspectJ 基于字节码操作(Bytecode Manipulation)。Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java ⽣态系统中最完整的 AOP 框架了。AspectJ 相⽐于 Spring AOP 功能更加强⼤,但是 Spring AOP 相对来说更简单,如果我们的切⾯⽐᫾少,那么两者性能差异不⼤。但是,当切⾯太多的话,最好选择 AspectJ ,它⽐Spring AOP 快很多。 ### 2.Spring MVC SpringMVC的运行流程 ![img](https://img-blog.csdnimg.cn/20190822210354552.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MDExNDE=,size_16,color_FFFFFF,t_70) ⑴ 用户发送请求至前端控制器DispatcherServlet ⑵ DispatcherServlet收到请求调用HandlerMapping处理器映射器。 ⑶ 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。 ⑷ DispatcherServlet通过HandlerAdapter处理器适配器调用处理器 ⑸ 执行处理器(Controller,也叫后端控制器)。 ⑹ Controller执行完成返回ModelAndView ⑺ HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet ⑻ DispatcherServlet将ModelAndView传给ViewReslover视图解析器 ⑼ ViewReslover解析后返回具体View ⑽ DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)。 ⑾ DispatcherServlet响应用户。 ### 3.Spring Bean的生命周期 基本流程 ![img](http://cdn.processon.com/5e7d6ab3e4b0ffc4ad40f430?e=1585281219&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:ODAsdLU8gFq_nOk8r195Tddev18=) **1.实例化**------Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化 **2.填充属性**-------Bean实例化后对将Bean的引入和值注入到Bean的属性中 **3.执行Aware接口**----Aware接口是Spring中的“觉醒”接口,是Spring容器通过回调向bean注入相关对象的接口, 1.BeanNameAware Spring将Bean的Id传递给setBeanName()方法 2.BeanFactoryAware 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入 3.ApplicationContextAware 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来 **4.初始化** 初始化是指完成bean的创建和依赖注入后进行的一个回调,可以利用这个回调进行一些自定义的工作,实现初始化的方式有三种 1.实现InitializingBean接口 2.使用@PostConstruct注解 3.xml中通过init-method属性指定初始化方法 **5.可用状态** 指bean已经准备就绪、可以被应用程序使用了,此时bean会一直存在于Spring容器中 **6.销毁** 指这个bean从Spring容器中消除,这个操作往往伴随着Spring容器的销毁 1.实现DisposableBean接口 2.使用@PreDestroy注解 3.xml中通过destroy-method属性指定 **7.扩展点** InstantiationAwareBeanPostProcessorAdapter 这个适配器是后置处理器接口BeanPostProcessor的子类, 实例化前的扩展点EP1 => postProcessBeforeInstantiation实例化后的扩展点EP2 => postProcessAfterInstantiation初始化前的扩展点EP3 => postProcessBeforeInitialization初始化后的扩展点EP4 => postProcessAfterInitialization对于这几个方法需要注意一下几点: ### 4.Spring循环依赖的问题 [https://blog.csdn.net/u014401141/article/details/108370872]: https://blog.csdn.net/u014401141/article/details/108370872 环调用其实就是一个死循环,除非有终结条件。 Spring中循环依赖场景有: (1)构造器的循环依赖 (2)field属性的循环依赖。 Spring的循环依赖的理论依据其实是基于Java的引用传递,当我们获取到对象的引用时,对象的field或则属性是可以延后设置的(但是构造器必须是在获取引用之前)。 Spring为了解决单例的循环依赖问题,使用了**三级缓存** 这三级缓存分别指: singletonFactories : 单例对象工厂的cache earlySingletonObjects :提前暴光的单例对象的Cache singletonObjects:单例对象的cache 我们在创建bean的时候,首先想到的是从cache中获取这个单例的bean,这个缓存就是singletonObjects。主要调用方法就就是: Spring首先从一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory.getObject()(三级缓存)获取,如果获取到了则:从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。 让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。 知道了这个原理时候,肯定就知道为啥Spring不能解决“A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象”这类问题了!因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。 ### 5.Spring用到的一些设计模式 [https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485303&idx=1&sn=9e4626a1e3f001f9b0d84a6fa0cff04a&chksm=cea248bcf9d5c1aaf48b67cc52bac74eb29d6037848d6cf213b0e5466f2d1fda970db700ba41&token=255050878&lang=zh_CN%23rd]: Spring 框架中用到了哪些设计模式: - **工厂设计模式** : Spring使用工厂模式通过 `BeanFactory`、`ApplicationContext` 创建 bean 对象。 - **代理设计模式** : Spring AOP 功能的实现。 - **单例设计模式** : Spring 中的 Bean 默认都是单例的。 - **模板方法模式** : Spring 中 `jdbcTemplate`、`hibernateTemplate` 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 - **包装器设计模式** : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 - **观察者模式:** Spring 事件驱动模型就是观察者模式很经典的一个应用。 - **适配器模式** :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。 ### 6.Spring 事务Spring 管理事务的⽅式有⼏种? 1. 编程式事务,在代码中硬编码。(不推荐使⽤) 2. 声明式事务,在配置⽂件中配置(推荐使⽤) 声明式事务⼜分为两种: 1. 基于XML的声明式事务 2. 基于注解的声明式事务 Spring 事务中的隔离级别有哪⼏种? **TransactionDefinition** 接⼝中定义了五个表示隔离级别的常量: **TransactionDefinition.ISOLATION_DEFAULT:** 使⽤后端数据库默认的隔离级别,Mysql 默认采 ⽤的 REPEATABLE_READ隔离级别 Oracle 默认采⽤的 READ_COMMITTED隔离级别. **TransactionDefinition.ISOLATION_READ_UNCOMMITTED:** 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 **TransactionDefinition.ISOLATION_READ_COMMITTED:** 允许读取并发事务已经提交的数据,可以阻⽌脏读,但是幻读或不可重复读仍有可能发⽣ **TransactionDefinition.ISOLATION_REPEATABLE_READ:** 对同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改,可以阻⽌脏读和不可重复读,但幻读仍有可能发⽣。 **TransactionDefinition.ISOLATION_SERIALIZABLE:** 最⾼的隔离级别,完全服从ACID的隔离级 别。所有的事务依次逐个执⾏,这样事务之间就完全不可能产⽣⼲扰,也就是说,该级别可以防 ⽌脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会⽤到该级别。 Spring 事务中哪⼏种事务传播⾏为? ⽀持当前事务的情况: **TransactionDefinition.PROPAGATION_REQUIRED**: 如果当前存在事务,则加⼊该事务;如果当前没有事务,则创建⼀个新的事务。 **TransactionDefinition.PROPAGATION_SUPPORTS**: 如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的⽅式继续运⾏。 **TransactionDefinition.PROPAGATION_MANDATORY**: 如果当前存在事务,则加⼊该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)不⽀持当前事务的情况: **TransactionDefinition.PROPAGATION_REQUIRES_NEW**: 创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。 **TransactionDefinition.PROPAGATION_NOT_SUPPORTED**: 以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起。 **TransactionDefinition.PROPAGATION_NEVER**: 以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。 其他情况: **TransactionDefinition.PROPAGATION_NESTED**: 如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。 @Transactional(rollbackFor = Exception.class)注解了解吗? 我们知道:Exception分为运⾏时异常RuntimeException和⾮运⾏时异常。事务管理对于企业应⽤来说是⾄关重要的,即使出现异常情况,它也可以保证数据的⼀致性。当 @Transactional 注解作⽤于类上时,该类的所有 public ⽅法将都具有该类型的事务属性,同时,我们也可以在⽅法级别使⽤该标注来覆盖类级别的定义。如果类或者⽅法加了这个注解,那么这个类⾥⾯的⽅法抛出异常,就会回滚,数据库⾥⾯的数据也会回滚。在 @Transactional 注解中如果不配置 rollbackFor 属性,那么事物只会在遇到 RuntimeException 的时候才会回滚,加上 rollbackFor=Exception.class ,可以让事物在遇到⾮运⾏时异常时也回滚。 ## 7.2 SpringBoot ### 1.SpringBoot的自动装配机制的基本原理 ![img](http://cdn.processon.com/5fe55f3f1e08531ceaaf712d?e=1608871248&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:9qLHmqfHTIbVHOs52ItQjOcjeWE=) 至此,自动装配的原理基本上就分析完了,简单来总结一下核心过程: @SpringBootApplication的注解中有一个是@EnableAutoConfiguration注解,这个注解有一个@Import({EnableAutoConfigurationImportSelector.class}),EnableAutoConfigurationImportSelector内部则是使用了SpringFactoriesLoader.loadFactoryNames方法进行扫描具有META-INF/spring.factories文件的jar包。 启动类上@SpringBootApplication -> 引入AutoConfigurationImportSelector -> ConfigurationClassParser 中处理 -> 获取spring.factories中EnableAutoConfiguration实现 •通过@Import ( AutoConfigurationlmportSelector)实现配置类的导入,但是这里并不是传统 意义上的单个配置类装配。 • AutoConfigurationlmportSelector类实现了ImportSelector接口,重写了方法selectlmports , 它用于实现选择慨量配置类的装配。 •通过Spring提供的SpringFactoriesLoader机制,扫描classpath路径下的META-INF/spring .factories ,读取需要实现自动装配的配置类。 •通过条件筛选的方式,把不符合条件的配置排除,最终完成自动装配。 ![img](http://cdn.processon.com/5e7437c9e4b01518202e3ca1?e=1584678361&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:hNiPxx7_bdCpH3zI2Hppr5Zx6pA=) ### 2.怎么去自定义实现一个starter 分为三步: 第一步:新建工程在pom文件里面加入依赖 4.0.0 org.springframework.boot spring-boot-starter-parent 2.2.6.RELEASE com.alen spring-boot-starter-price 0.0.1-SNAPSHOT 1.8 org.springframework.boot spring-boot-autoconfigure org.springframework.boot spring-boot-configuration-processor maven-compiler-plugin 1.8 1.8 UTF-8 第二步 编写创建Configuration类。增加注解@Configuration,@EnableConfigurationProperties, @ConditionalOnProperty等 第三步 spring.factories中添加自动配置类实现 ### 3.SpringBoot应用的启动过程 流程 第一部分new SpringApplication,进行SpringApplication的初始化模块,配置一些基本的环境变量、资源、构造器、实例化所有监听器和工厂类。 第二部分实现了应用具体的启动方案,包括启动流程的监听模块、加载配置环境模块、及核心的创建上下文环境模块 run--这个方法里面执行的: 第一步:获取并启动监听器,springboot通过调用所有封装好的运行监听器的方法来调用广播器发布各种事件。 第二步:构造容器环境 第三步:创建容器 第四步:实例化SpringBootExceptionReporter.class,用来支持报告关于启动的错误 第五步:配置初始化容器上下文,调用系统初始化类的初始化方法,发布事件, 第六步:刷新容器 refreshContext(context)方法(初始化方法如下)将是实现spring-boot-starter-*(mybatis、redis等)自动化配置的关键,包括spring.factories的加载,bean的实例化等核心工作,获取所有的配置类。 第七步:刷新容器后的扩展接口,注册钩子,启动tomact. 第三部分是自动化配置模块,该模块作为springboot自动配置核心 ![img](http://cdn.processon.com/5e7432ffe4b027d999bd0161?e=1584677136&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:a9PebEKSGAaYp22exN6O2ejzMqM=) ### 4.@SpringBootApplication注解的意思 SpringBootApplication其实就是以下三个注解的总和 @Configuration: 用于定义一个配置类 @EnableAutoConfiguration :Spring Boot会自动根据你jar包的依赖来自动配置项目。 @ComponentScan: 告诉Spring 哪个packages 的用注解标识的类 会被spring自动扫描并且装入bean容器。 ## 7.3 SpringCloud ### 1.Eureka #### 1.Eureka核心概念 ##### Eureka Client 角色 ###### Eureka Server:注册中心服务端 服务注册 服务提供者启动时,会通过 Eureka Client 向 Eureka Server 注册信息,Eureka Server 会存储该服务的信息,Eureka Server 内部有二层缓存机制来维护整个注册表 提供注册表 服务消费者在调用服务时,如果 Eureka Client 没有缓存注册表的话,会从 Eureka Server 获取最新的注册表 同步状态 Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态。 ###### Eureka Client:注册中心客户端 Eureka Client 是一个 Java 客户端,用于简化与 Eureka Server 的交互。Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是当服务有更改的时候会出现信息不一致。 Register: 服务注册 服务的提供者,将自身注册到注册中心,服务提供者也是一个 Eureka Client。当 Eureka Client 向 Eureka Server 注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL,主页等。 Renew: 服务续约 Eureka Client 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka Server 该 Eureka Client 运行正常,没有出现问题。 默认情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的续约,Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更改。 服务续约的两个重要属性 Eviction 服务剔除 当 Eureka Client 和 Eureka Server 不再有心跳时,Eureka Server 会将该服务实例从服务注册列表中删除,即服务剔除。 Cancel: 服务下线 Eureka Client 在程序关闭时向 Eureka Server 发送取消请求。 发送请求后,该客户端实例信息将从 Eureka Server 的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容: Remote Call: 远程调用 当 Eureka Client 从注册中心获取到服务提供者信息后,就可以通过 Http 请求调用对应的服务;服务提供者有多个时,Eureka Client 客户端会通过 Ribbon 自动进行负载均衡。 ##### Eureka 分区 Eureka 提供了 Region 和 Zone 两个概念来进行分区,这两个概念均来自于亚马逊的 AWS: region:可以理解为地理上的不同区域,比如亚洲地区,中国区或者深圳等等。没有具体大小的限制。根据项目具体的情况,可以自行合理划分 region。 zone:可以简单理解为 region 内的具体机房,比如说 region 划分为深圳,然后深圳有两个机房,就可以在此 region 之下划分出 zone1、zone2 两个 zone。 上图中的 us-east-1c、us-east-1d、us-east-1e 就代表了不同的 Zone。Zone 内的 Eureka Client 优先和 Zone 内的 Eureka Server 进行心跳同步,同样调用端优先在 Zone 内的 Eureka Server 获取服务列表,当 Zone 内的 Eureka Server 挂掉之后,才会从别的 Zone 中获取信息。 ##### Eureka如何保证AP Eureka Server 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka Client 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台 Eureka Server 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。 #### 2.自我保护机制 默认情况下,如果 Eureka Server 在一定的 90s 内没有接收到某个微服务实例的心跳,会注销该实例。但是在微服务架构下服务之间通常都是跨进程调用,网络通信往往会面临着各种问题,比如微服务状态正常,网络分区故障,导致此实例被注销。 固定时间内大量实例被注销,可能会严重威胁整个微服务架构的可用性。为了解决这个问题,Eureka 开发了自我保护机制,那么什么是自我保护机制呢? Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 即会进入自我保护机制。 原理 Eureka Server 进入自我保护机制,会出现以下几种情况: (1 Eureka 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务 (2 Eureka 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用) (3 当网络稳定时,当前实例新的注册信息会被同步到其它节点中 Eureka 自我保护机制是为了防止误杀服务而提供的一个机制。当个别客户端出现心跳失联时,则认为是客户端的问题,剔除掉客户端;当 Eureka 捕获到大量的心跳失败时,则认为可能是网络问题,进入自我保护机制;当客户端心跳恢复时,Eureka 会自动退出自我保护机制。 如果在保护期内刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,即会调用失败。对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。 通过在 Eureka Server 配置如下参数,开启或者关闭保护机制,生产环境建议打开: eureka.server.enable-self-preservation=true #### 3.Eureka 集群原理 Eureka Server 集群相互之间通过 Replicate 来同步数据,相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。 如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点。当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 当前所知的所有节点中。 另外 Eureka Server 的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。所以,如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过 P2P 的方式完成服务注册表的同步。 Eureka Server 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。 ![img](http://cdn.processon.com/5e8f20951e085370359bb1a1?e=1586441893&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:sgHfzXT1-7WOms5CBPhjRcqbKaE=) #### 4.Eurka 工作流程 我们来整体梳理一下 Eureka 的工作流程: 1、Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息 2、Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务 3、Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常 4、当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例 5、单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端 6、当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式 7、Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地 8、服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存 9、Eureka Client 获取到目标服务器信息,发起服务调用 10、Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除 这就是Eurka基本工作流程 ### 2.nacos服务注册与配置中心 官网地址:https://nacos.io/zh-cn/docs/quick-start.html #### 1.Nacos是什么 Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。 Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。 服务(Service)是 Nacos 世界的一等公民。Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理: **Nacos 的关键特性包括:** **服务发现和服务健康监测** Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务。 Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP)和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。 对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测2种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助您根据健康状态管理服务的可用性及流量。 **动态配置服务** 动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。 动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。 配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。 Nacos 提供了一个简洁易用的UI (控制台样例 Demo) 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。 **动态 DNS 服务** 动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。动态DNS服务还能让您更容易地实现以 DNS 协议为基础的服务发现,以帮助您消除耦合到厂商私有服务发现 API 上的风险。 Nacos 提供了一些简单的 DNS APIs TODO 帮助您管理服务的关联域名和可用的 IP:PORT 列表. **服务及其元数据管理** Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。 #### 2.Nacos原理分析 ##### 1.Nacos架构 ![img](http://cdn.processon.com/5f50ea96e0b34d6f59dda1d4?e=1599142055&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:2ToWhWibK3hQPtoir_B59xRQNQA=) ##### 2.基本架构及概念 **服务 (Service)** 服务是指一个或一组软件功能(例如特定信息的检索或一组操作的执行),其目的是不同的客户端可以为不同的目的重用(例如通过跨进程的网络调用)。Nacos 支持主流的服务生态,如 Kubernetes Service、gRPC|Dubbo RPC Service 或者 Spring Cloud RESTful Service. **服务注册中心 (Service Registry)** 服务注册中心,它是服务,其实例及元数据的数据库。服务实例在启动时注册到服务注册表,并在关闭时注销。服务和路由器的客户端查询服务注册表以查找服务的可用实例。服务注册中心可能会调用服务实例的健康检查 API 来验证它是否能够处理请求。 服务元数据 (Service Metadata) 服务元数据是指包括服务端点(endpoints)、服务标签、服务版本号、服务实例权重、路由规则、安全策略等描述服务的数据 **服务提供方 (Service Provider)** 是指提供可复用和可调用服务的应用方 **服务消费方 (Service Consumer)** 是指会发起对某个服务调用的应用方 **配置 (Configuration)** 在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。 **配置管理 (Configuration Management)** 在数据中心中,系统中所有配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动统称为配置管理。 **名字服务 (Naming Service)** 提供分布式系统中所有对象(Object)、实体(Entity)的“名字”到关联的元数据之间的映射管理服务,例如 ServiceName -> Endpoints Info, Distributed Lock Name -> Lock Owner/Status Info, DNS Domain Name -> IP List, 服务发现和 DNS 就是名字服务的2大场景。 使用Raft算法,支持主备模式,所以底层使用数据一致性算法来完成从节点的数据同步 **配置服务 (Configuration Service)** 在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。 ##### 3.逻辑架构及其组件介绍 ![img](http://cdn.processon.com/5f50ec95e0b34d6f59dda4e8?e=1599142566&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:2gUA1sBUZJEnIdkp1xSGqzdtMrA=) ##### 4.注册中心的原理 服务注册流程 1.服务实例在启动时注册到服务注册表中 2.服务消费者查询服务注册表,获取可用实例 3.服务注册中心需要调用服务实例的健康检查API来验证它是否能够处理请求 ![img](http://cdn.processon.com/5f5305b77d9c08028bd57556?e=1599280071&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:n_INjAj-k85axk-sEcKLa6wSXDY=) ###### 1.服务注册 SpringCloud什么时候完成了服务注册 Spring-Cloud-Common里面有个ServiceRegistry是服务注册的标准接口 Nacos的实现类是NacosSerciceRegistry SpringBoot的自动准配机制启动时会扫描 Spring-Cloud-Common包里面的spring.factories文件里面的AutoServiceRegistrationAuotoConfiguratio的配置类,在容器初始化完的事件会触发NacosSerciceRegistry.register方法进行服务注册, 服务注册主要的逻辑是 调用Nacos Client SDK中的nameServiice.registerInstance完成服务的注册 这个方法里面的实现 1.创建心跳实现健康监测 就是服务生产者通过定时器定时5s给Name Sever发送心跳,Name Sever启动线程进行定时监测,服务端根据心跳包更新服务端的服务状态 2.添加服务注册信息 总结: Nacos客户端通过Open API的形式发送服务注册请求 Nacos服务端收到请求后,做以下三件事: 构建一个Service对象保存到ConcurrentHashMap集合中 使用定时任务对当前服务下的所有实例建立心跳检测机制 基于数据一致性协议服务数据进行同步 ![img](http://cdn.processon.com/5f5305a607912902cf743e00?e=1599280054&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:rrxOt-37CEImNyNrI1Cq3NtK-5o=) ###### 2.服务地址的获取 服务提供者地址查询,调用NameServer的服务列表查询接口查询服务信息 ###### 3.服务地址变化的感知 可以通过subscribe方法来实现监听,其中serviceName表示服务名、EventListener表示监听到的事件: ![img](http://cdn.processon.com/5f53058e5653bb53ea917636?e=1599280030&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:fAV1WT9WJ42aqi3-Yv2StbN9gM0=) ##### 5.Nacos配置管理原理分析 配置中心的大致架构,用户可以通过管理平台发布配置,通过 HTTP 调用将配置注册到服务端,服务端将之保存在 MySQL 等持久化存储引擎中;用户通过客户端 SDK 访问服务端的配置,同时建立 HTTP 的长轮询监听配置项变更,同时为了减轻服务端压力和保证容灾特性,配置项拉取到客户端之后会保存一份快照在本地文件中,SDK 优先读取文件里的内容。 对于Nacos Config来说,其实就是提供了配置的集中式管理功能,然后对外提供CRUD的访问接口使得应用系统可以完成配置的基本操作。实际上这种场景并不复杂,对于服务端来说,无非就是配置如何存储,以及是否需要持久化,对于客户端来说,就是通过接口从服务器端查询到相应的数据,然后返回即可。 ![img](http://cdn.processon.com/5f5328981e08531762c0ce3c?e=1599289000&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:6eY_8nYyTRfnBMQbb7gpuB3Bl4s=) ###### 1.客户端核心源码解析 **动态监听** 当Nacos Config Server上的配置发生变化时,需要让相关的应用程序感知配置的变化进而感知应用的变化,这就需要客户端针对感兴趣的配置实现监听。 那么Nacos客户端是如何实现配置变更的实时更新的呢?一般来说 ,客户端和服务端之间的数据交互无非两种方式: Pull和Push。 Pull表示客户端从服务端主动拉取数据。 Push表示服务端主动把数据推送到客户端。 这两种方式没有什么优劣之分,只是看哪种方式更适合于当前的场景。比如ActiveMQ就支持Push和Pull两种模式,用户可以在特定场景选择不同的模式来实现消费端消息的获取。 对于Push模式来说,服务端需要维持与客户端的长连接,如果客户端的数量比较多,那么服务端需要耗费大量的内存资源来保存每个连接,并且为了检测连接的有效性,还需要心跳机制来维持每个连接的状态。 在Pull模式下,客户端需要定时从服务端拉取一次数据,由于定时任务会存在一定的时间间隔,所以不能保证数据的实时性。并且在服务端配置长时间不更新的情况下,客户端的定时任务会做一些无效的Pull。 Nacos采用的是Pull模式,但并不是简单的Pull ,而是一种长轮询机制,它结合Push和Pull两者的优势。客户端采用长轮询的方式定时发起Pull请求,去检查服务端配置信息是否发生了变更,如果发生了变更,则客户端会根据变更的数据获得最新的配置。所谓长轮询,是客户端发起轮询请求之后,服务端如果有配置发生变更,就直接返回。 如果客户端发起Pull请求后,发现服务端的配置和客户端的配置是保持一致的,那么服务端会先"Hold" 住这个请求,也就是服务端拿到这个连接之后在指定的时间段内一直不返回结果,直到这段时间内配置发生变化,服务端会把原来"Hold" 住的请求进行返回,客户端的主要流程是SpringBoot在启动子的时候会请求远程的NacosConfig服务端加载配置文件,当通过 HTTP 获取远端配置时,Nacos 提供了两种熔断策略,一是超时时间,二是最大重试次数,默认重试三次,同时将配置信息保存到本地缓存,所有的 CacheData 都保存在 ClientWorker 类中的原子 cacheMap 中,同时启动ClientWorker ,ClientWorker 通过其下的两个线程池完成配置长轮询的工作,一个是单线程的 executor,每隔 10ms 按照每 3000 个配置项为一批次捞取待轮询的 cacheData 实例,将其包装成为一个LongPollingTask 提交进入第二个线程池 executorService 处理。 该长轮询任务内部主要分为四步: 检查本地配置,忽略本地快照不存在的配置项,检查是否存在需要回调监听器的配置项如果本地没有配置项的,从服务端拿,返回配置内容发生变更的键值列表每个键值再到服务端获取最新配置,更新本地快照,补全之前缺失的配置检查 MD5 标签是否一致,不一致需要回调监听器 如果该轮询任务抛出异常,等待一段时间再开始下一次调用,减轻服务端压力。另外,Nacos 在 HTTP 工具类中也有限流器的代码,通过多种手段降低轮询或者大流量情况下的风险。下文还会讲到,如果在服务端没有发现变更的键值,那么服务端会夯住这个 HTTP 请求一段时间(客户端侧默认传递的超时是 30s),以此进一步减轻客户端的轮询频率和服务端的压力。 ![img](http://cdn.processon.com/5f532c0e5653bb53ea919ea1?e=1599289887&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:1l3DO7tztmoaPPr8DWMyaM8ghsA=) ###### 2.服务端主要流程 ![img](http://cdn.processon.com/5f532a4f1e08531762c0d057?e=1599289439&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:Bq0bVuVBqqT9zj-w8cD-7HqGopo=) Nacos服务端收到请求之后,先检查配置是否发生了变更 ,如果没有 ,则设置一个定时任务,延期29.5s执行 ,并且把当 前的客户端长轮询连接加入allSubs队列。这时候有两种方式触发该连接结果的返回: 第一种是在等待29.5s后触发自动检查机制,这时候不管配置有没有发生变化,都会把结果返回客户端。而29.5s就是这个长连接保持的时间。 第二种是在29.5s内任意一个时刻,通过Nacos Dashboard或者API的方式对配置进行了修改,这会触发一个事件机制,监听到该事件的任务会遍历allSubs队列,找到发生变更的配置项对应的ClientLongPolling任务,将变更的数据通过该任务中的连接进行返回,就完成了一次"推送"操作。 这样既能保证客户端实时感知配置的变化,也降低了服务端的压力。其中,这个长连接的会话超时时间默认是30s。 ### 3.服务限流与熔断:sentinel Sentinel 是面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。 使用滑动计数器限流 ![img](http://cdn.processon.com/5fefe9047d9c0863d3035cec?e=1609561876&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:KccMtC2HypyPuYIG0uN-ZNFBors=) #### 1.Sentinel基本概念 **资源** 资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。 只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。 **规则** 围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。 Sentinel功能和设计理念 #### 2.什么是流量控制 流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状 流量控制设计理念 流量控制有以下几个角度: 资源的调用关系,例如资源的调用链路,资源和资源之间的关系; 运行指标,例如 QPS、线程池、系统负载等; 控制的效果,例如直接限流、冷启动、排队等。 Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。 ![img](http://cdn.processon.com/5fefea9f07912977bedcab1a?e=1609562287&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:bWs_TsrfwK-62GxFSbrnDpzxg24=) #### 3.工作原理分析 ![img](http://cdn.processon.com/5f5c3e89e0b34d6f59ef6850?e=1599884442&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:j3l1oa7R1DIOAkf1IHSaHOv0eHA=) Sentinel的核心分为三部分:工作流程、数据结构和限流算法,工作原理 可以看出,调用链路是Sentinel的工作主流程,由各个Slot插槽组成,将不同的Slot按照顺序串在一起(责任 链模式),从而将不同的功能(限流、降级、系统保护)组合在一起。Sentinel中各个Slot承担了不同的职责,例如LogSlot负责记录日志、StatisticSlot负责统计指标数据、FlowSlot负责限流等。这是一职责分离的设计,每个模块更聚焦于实现某个功能。 在Sentinel中,所有的资源都对应4资源名称(resourceName),每次访问该资源都会创建4Entry对 象,在创建Entry的同时,会创建一系列功能槽(Slot Chain ),这些槽会组成一个责任链,每个槽负责不同的职责。 • NodeSelectorSlot :负责收集资源的调用路径,以树状结构存储调用栈,用于根据调用路径来限 流降级。 • ClusterBuilderSlot :负责创建以资源名维度统计的ClusterNode ,以及创建每个ClusterNode 下按调用来源origin划分的StatisticNode。 • LogSlot :在出现限流、熔断、系统保护时负责记录日志。 • AuthoritySlot :权限控制,支持黑名单和白名单两种策略。 • SystemSlot :控制总的入口流量,限制条件依次是总QPS、总线程数、叮阈值、操作系统当前loadl、操作系统当前CPU利用率。 • FlowSlot :根据限流规则和各个Node中的统计觀进行限流判断。 • DegradeSlot :根据焰断规则和各个Node中的统计数据逬行服务降级。 • StatisticSIot :统计不同维度的请求数、通过数、限流数、线程数等信息 ### 4.断路器Hystrix Feign是自带断路器的 #### 1.Hystix原理 熔断器模式 ![img](http://cdn.processon.com/5e80536ae4b027d999d59592?e=1585471866&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:ic5KQ67rfj366po-y7K4H-dtzY0=) **Hystrix的内部处理逻辑** Hystrix使用命令模式(继承HystrixCommand类或者是HystrixObservableCommand类)来包裹具体的服务调用逻辑(run方法), 并在命令模式中添加了服务调用失败后的降级逻辑(getFallback). 同时我们在Command的构造方法中可以定义当前服务线程池和熔断器的相关参数. ![img](http://cdn.processon.com/5e805373e4b03b99653e707f?e=1585471875&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:r1ME9zdSPKkyvEWtVFfKMKO5dw4=) Hystrix检查当前服务的熔断器开关是否开启, 若开启, 则执行降级服务getFallback方法. 若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否能接收新的请求, 若超过线程池已满, 则执行降级服务getFallback方法. 若线程池接受请求, 则Hystrix开始执行服务调用具体逻辑run方法. 若服务执行失败, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况. 若服务执行超时, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况. 若服务执行成功, 返回正常结果. 若服务降级方法getFallback执行成功, 则返回降级结果. 若服务降级方法getFallback执行失败, 则抛出异常. **Hystrix Metrics的实现** Hystrix的Metrics中保存了当前服务的健康状况, 包括服务调用总次数和服务调用失败次数等. 根据Metrics的计数, 熔断器从而能计算出当前服务的调用失败率, 用来和设定的阈值比较从而决定熔断器的状态切换逻辑. 因此Metrics的实现非常重要. 1.4之前的滑动窗口实现 Hystrix在这些版本中的使用自己定义的滑动窗口数据结构来记录当前时间窗的各种事件(成功,失败,超时,线程池拒绝等)的计数. 事件产生时, 数据结构根据当前时间确定使用旧桶还是创建新桶来计数, 并在桶中对计数器经行修改. 这些修改是多线程并发执行的, 代码中有不少加锁操作,逻辑较为复杂. #### 2.使用Fallback() 提供降级策略 以下四种情况将触发getFallback调用: (1):run()方法抛出非HystrixBadRequestException异常。 (2):run()方法调用超时 (3):熔断器开启拦截调用 (4):线程池/队列/信号量是否跑满 #### 3.Hystrix隔离策略 线程池(默认) ![img](http://cdn.processon.com/5e8f2c8d5653bb6e6ec24969?e=1586444957&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:Yk8T8ne1xdJBaSun81t2B65SiBo=) 图的左边2/3是线程池资源隔离示意图,右边的1/3是信号量资源隔离示意图,我们先来看左边的示意图。 当用户请求服务A和服务I的时候,tomcat的线程(图中蓝色箭头标注)会将请求的任务交给服务A和服务I的内部线程池里面的线程(图中橘色箭头标注)来执行,tomcat的线程就可以去干别的事情去了,当服务A和服务I自己线程池里面的线程执行完任务之后,就会将调用的结果返回给tomcat的线程,从而实现资源的隔离,当有大量并发的时候,服务内部的线程池的数量就决定了整个服务的并发度,例如服务A的线程池大小为10个,当同时有12请求时,只会允许10个任务在执行,其他的任务被放在线程池队列中,或者是直接走降级服务,此时,如果服务A挂了,就不会造成大量的tomcat线程被服务A拖死,服务I依然能够提供服务。整个系统不会受太大的影响。 信号量 ![img](http://cdn.processon.com/5e8f2d14e0b34d4820efc229?e=1586445092&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:zKnTIqAM9jDi1LFODcUa5Pj9hAw=) ## 7.4 Zookeeper 一个分布式的服务协调组件,zookeeper维护了一个类似文件系统的数据结构, 通知机制:客户端监听它关心的目录节点,节点变化时通知客户端 ![img](http://cdn.processon.com/5e69e35ae4b0e3993b5f29b8?e=1584001386&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:zEbFUZ5n8O693n49eWQmJh0oqG4=) ### 1.ZooKeeper的Zab一致性协议 #### 什么是Zab协议 Zab协议 的全称是 Zookeeper Atomic Broadcast (Zookeeper原子广播),是为分布式协调服务Zookeeper专门设计的一种 支持崩溃恢复 的 原子广播协议 ,Zookeeper保证数据一致性的核心算法。通过 Zab 协议来保证分布式事务的最终一致性。 #### Zab协议需要做到什么 1)Zab 协议需要确保那些已经在 Leader 服务器上提交(Commit)的事务最终被所有的服务器提交。 2)Zab 协议需要确保丢弃那些只在 Leader 上被提出而没有被提交的事务。 #### Zab协议的基本使用情景 ##### 崩溃恢复 Zab 的四个阶段: 1.选举阶段(Leader Election) 节点在一开始都处于选举节点,只要有一个节点得到超过半数节点的票数,它就可以当选准 Leader,这一阶段的目的就是为了选出一个准 Leader ,然后进入下一个阶段。 ![img](http://cdn.processon.com/5e6cf3a5e4b0e3993b64b40b?e=1584202166&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:tJkoszL3YvWa-nxXC3EVXCc3rOA=) 2.发现阶段(Descovery) 在这个阶段,Followers 和上一轮选举出的准 Leader 进行通信,同步 Followers 最近接收的事务 Proposal 。 一个 Follower 只会连接一个 Leader,如果一个 Follower 节点认为另一个 Follower 节点,则会在尝试连接时被拒绝。被拒绝之后,该节点就会进入 Leader Election阶段。 这个阶段的主要目的是发现当前大多数节点接收的最新 Proposal,并且准 Leader 生成新的 epoch ,让 Followers 接收,更新它们的 acceptedEpoch。 3.同步阶段(Synchronization) 同步阶段主要是利用 Leader 前一阶段获得的最新 Proposal 历史,同步集群中所有的副本。 只有当 quorum(超过半数的节点) 都同步完成,准 Leader 才会成为真正的 Leader。Follower 只会接收 zxid 比自己 lastZxid 大的 Proposal。 4、广播阶段(Broadcast) 到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且 Leader 可以进行消息广播。同时,如果有新的节点加入,还需要对新节点进行同步。 需要注意的是,Zab 提交事务并不像 2PC 一样需要全部 Follower 都 Ack,只需要得到 quorum(超过半数的节点)的Ack 就可以。 ![img](http://cdn.processon.com/5e6cf468e4b09b0f79ea567b?e=1584202361&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:7WI8tfyU9kry_Wciuu_RE3Qaq-g=) ###### zookeeper的zab协议的具体实现 1.选举(Fast Leader Election) 前面提到的 FLE 会选举拥有最新Proposal history (lastZxid最大)的节点作为 Leader,这样就省去了发现最新提议的步骤。这是基于拥有最新提议的节点也拥有最新的提交记录 成为 Leader 的条件: 1)选 epoch 最大的 2)若 epoch 相等,选 zxid 最大的 3)若 epoch 和 zxid 相等,选择 server_id 最大的(zoo.cfg中的myid) 节点在选举开始时,都默认投票给自己,当接收其他节点的选票时,会根据上面的 Leader条件 判断并且更改自己的选票,然后重新发送选票给其他节点。当有一个节点的得票超过半数,该节点会设置自己的状态为 Leading ,其他节点会设置自己的状态为 Following。 ![img](http://cdn.processon.com/5e6cf603e4b09b0f79ea5879?e=1584202772&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:snFbbZ64p3gApLTOozgGyskIdsk=) 2.恢复(Recovery Phase) 这一阶段 Follower 发送他们的 lastZxid 给 Leader,Leader 根据 lastZxid 决定如何同步数据。这里的实现跟前面的 Phase 2 有所不同:Follower 收到 TRUNC 指令会终止 L.lastCommitedZxid 之后的 Proposal ,收到 DIFF 指令会接收新的 Proposal。 history.lastCommitedZxid:最近被提交的 Proposal zxid history.oldThreshold:被认为已经太旧的已经提交的 Proposal zxid ![img](http://cdn.processon.com/5e6cf660e4b0dab55409270a?e=1584202864&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:TRue1b5a_q-9uLjDXGsVz3mUaC8=) 3.广播(Broadcast Phase) ##### 消息广播 当选举产生了新的 Leader,同时集群中有过半的机器与该 Leader 服务器完成了状态同步(即数据同步)之后,Zab协议就会退出崩溃恢复模式。 这时,如果有一台遵守Zab协议的服务器加入集群,因为此时集群中已经存在一个Leader服务器在广播消息,那么该新加入的服务器自动进入恢复模式:找到Leader服务器,并且完成数据同步。同步完成后,作为新的Follower一起参与到消息广播流程中。 实现原理 当选举产生了新的 Leader,同时集群中有过半的机器与该 Leader 服务器完成了状态同步(即数据同步)之后,Zab协议就会退出崩溃恢复模式。 这时,如果有一台遵守Zab协议的服务器加入集群,因为此时集群中已经存在一个Leader服务器在广播消息,那么该新加入的服务器自动进入恢复模式:找到Leader服务器,并且完成数据同步。同步完成后,作为新的Follower一起参与到消息广播流程中。 ![img](http://cdn.processon.com/5e6cf2a0e4b0dab55409221e?e=1584201904&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:WBaogir1G-EU7t1hmpxu2l_xSkc=) ### 2.事务请求的处理方式 1)所有的事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被叫做 Leader服务器。其他剩余的服务器则是 Follower服务器。 2)Leader服务器 负责将一个客户端事务请求,转换成一个 事务Proposal,并将该 Proposal 分发给集群中所有的 Follower 服务器,也就是向所有 Follower 节点发送数据广播请求(或数据复制) 3)分发之后Leader服务器需要等待所有Follower服务器的反馈(Ack请求),在Zab协议中,只要超过半数的Follower服务器进行了正确的反馈后(也就是收到半数以上的Follower的Ack请求),那么 Leader 就会再次向所有的 Follower服务器发送 Commit 消息,要求其将上一个 事务proposal 进行提交。 ![img](http://cdn.processon.com/5e6cf071e4b01853042163f2?e=1584201345&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:EJHaA08eL1Ph3Mj3mSCSYcY96f4=) Zookeeper保证CP ## 7.5 Dubbo Dubbo是阿里巴巴开源的基于 Java 的高性能 RPC(一种远程调用) 分布式服务框架(SOA),致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。 dubbo采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务,所以基于这一点可以抽象出服务提供方(Provider)和服务消费方(Consumer)两个角色。关于注册中心、协议支持、服务监控等内容 ### 1.服务调用 注册中心启动,监听消息提供者的注册服务、接收消息消费者的服务订阅 (服务注册与发现机制)。 服务提供方发布服务到服务注册中心; 服务消费方从服务注册中心订阅服务; 服务消费方调用已经注册的可用服务; ![img](http://cdn.processon.com/5e69e8fee4b0e3993b5f46ec?e=1584002830&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:4r_YRyf8j4ynJ-meH6YjLVSf-iI=) **服务定义** 服务是围绕服务提供方和服务消费方的,服务提供方实现服务,而服务消费方调用服务 **服务注册** 服务注册中心可以通过特定协议来完成服务对外的统一。Dubbo提供的注册中心有如下几种类型可供选择: Multicast注册中心 Zookeeper注册中心 Redis注册中心 Simple注册中心 **服务监控** 无论是服务提供方,还是服务消费方,他们都需要对服务调用的实际状态进行有效的监控,从而改进服务质量。 **远程通信与信息交换** 远程通信需要指定通信双方所约定的协议,在保证通信双方理解协议语义的基础上,还要保证高效、稳定的消息传输。Dubbo继承了当前主流的网络通信框架,主要包括如下几个: Mina Netty Grizzly **RPC** ![img](http://cdn.processon.com/5e69eceee4b0f2f3bd1adbf9?e=1584003838&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:W7q8P1iAPgdiqPuJnfXmwC8jK74=) ### **2.设计模型** ![img](http://cdn.processon.com/5e69ed22e4b0e3993b5f5cde?e=1584003890&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:QjwoIB1KFUybbM79fjeiK2JA6CA=) ### 3.重试策略 在实际应用中查询语句容错策略建议使用默认 Failover Cluster **Failover Cluster** 失败自动切换,当出现失败,重试其它服务器。(默认) **Failfast Cluster** 快速失败,只发起一次调用,失败立即报错。 通常用于非幂等性的写操作,比如新增记录。 **Failsafe Cluster** 失败安全,出现异常时,直接忽略。 通常用于写入审计日志等操作。 **Failback Cluster** 失败自动恢复,后台记录失败请求,定时重发。 通常用于消息通知操作。 **Forking Cluster** 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。 可通过 forks=”2”来设置最大并行数。 **Broadcast Cluster** 广播调用所有提供者,逐个调用,任意一台报错则报错。(2.1.0 开始支持) 通常用于通知所有提供者更新缓存 或日志等本地资源信息。 **Dubbo 的连接方式** Dubbo 的客户端和服务端有三种连接方式,分别是:广播,直连和使用 zookeeper 注册中心。 ### 4.Dubbo的连接方式 Dubbo 的客户端和服务端有三种连接方式,分别是:广播,直连和使用 zookeeper 注册中心。 ### 5.负载均衡算法 dubbo提供的四种负载均衡策略 **加权随机算法 Random LoadBalance** 它的算法思想很简单。假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。 **加权轮询负载均衡 RandomLoadBalance** 所谓轮询是指将请求轮流分配给每台服务器。举个例子,我们有三台服务器 A、B、C。我们将第一个请求分配给服务器 A,第二个请求分配给服务器 B,第三个请求分配给服务器 C,第四个请求再次分配给服务器 A。这个过程就叫做轮询。轮询是一种无状态负载均衡算法,实现简单,适用于每台服务器性能相近的场景下。但现实情况下,我们并不能保证每台服务器性能均相近。如果我们将等量的请求分配给性能较差的服务器,这显然是不合理的。因此,这个时候我们需要对轮询过程进行加权,以调控每台服务器的负载。经过加权后,每台服务器能够得到的请求数比例,接近或等于他们的权重比。比如服务器 A、B、C 权重比为 5:2:1。那么在8次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的2次请求,服务器 C 则收到其中的1次请求。 **最小活跃数负载均衡 LeastActive LoadBalance** 活跃数指调用前后计数差,优先调用高的,相同活跃数的随机。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 **ConsistentHash LoadBalance** 一致性 Hash,相同参数的请求总是发到同一提供者。 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 ## 7.6 Redis Redis:REmote DIctionary Server(远程字典服务器),是完全开源免费的,用C语言编写的,遵守BSD协议,是一个高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一,也被人们称为数据结构服务器。 ### 1.NoSQL聚合数据模型 NoSQL技术与传统的关系型数据库相比,一个最明显的转变就是抛弃了关系模型。每种NoSQL解决方案的模型都是不同的。下面吧NoSQL生态系统的广泛使用的模型分为四类:“键值”(hashtable)、“文档”.,"列族"和“图”前三类数据库模型有一个共同特征,我们称其为“面向聚合”。 面向聚合 聚合:在”领域驱动设计“中,我们想把一组相互关联的对象视为一个整体单元来操作,而这个单元就叫聚合。我们通过原子操作更新聚合的值,并且在与数据存储通信时,也是以聚合为单位。选用面向聚合模型的决定性因素,在于它非常适合在集群上运行。这也是NoSQL崛起的关键。 特点 聚合数据模型的特点就是把经常访问的数据放在一起(聚合在一块);当然,以这种方式存储不可避免的会有重复,重复是为了更少的交互; 键值(Key-Value)存储数据库 Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB,memcache。 列存储数据库 Cassandra, HBase, Riak.是按列存储数据的。最大的特点是方便存储结构化和半结构化数据,方便做数据压缩,对针对某一列或者某几列的查询有非常大的IO优势。 图形(Graph)数据库 图形结构的数据库同其他行列以及刚性结构的SQL数据库不同,它是使用灵活的图形模型,并且能够扩展到多个服务器上Neo4J, InfoGrid。 文档型数据库 MongoDB ### 2.Redis优点 异常快速 支持丰富的数据类型 操作都是原子的 ### 3.Redis快的主要原因 1.完全基于内存 数据结构简单,对数据操作也简单 2.使用多路 I/O 复用模型,多路 I/O 复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。 ### 4.Redis数据类型 String,Hash(哈希)键值对集合,List(列表),Set(集合),Zset 有序集合 ### 5.Redis持久化 RDB模式是redis中默认的持久化策略. RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照 Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。\ AOF AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 优势 每修改同步:appendfsync always 同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好 每秒同步:appendfsync everysec 异步操作,每秒记录 如果一秒内宕机,有数据丢失 不同步:appendfsync no 从不同步 劣势 相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb,aof运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同 ### 6.Redis数据过期策略详解 **定时删除** 在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除. **惰性删除** key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null **定期删除** 每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key操作 **Redis采用的过期策略** 惰性删除+定期删除 ### 7.Redis集群方式 **主从模式** 主从模式:是三种集群方式里最简单的。它主要是基于Redis的主从复制特性架构的。通常我们会设置一个主节点,N个从节点;默认情况下,主节点负责处理使用者的IO操作,而从节点则会对主节点的数据进行备份,并且也会对外提供读操作的处理。主要的特点如下: 主从模式下,当某一节点损坏时,因为其会将数据备份到其它Redis实例上,这样做在很大程度上可以恢复丢失的数据。 主从模式下,可以保证负载均衡,这里不再叙说了 主从模式下,主节点和从节点是读写分离的。使用者不仅可以从主节点上读取数据,还可以很方便的从从节点上读取到数据,这在一定程度上缓解了主机的压力。 从节点也是能够支持写入数据的,只不过从从节点写入的数据不会同步到主节点以及其它的从节点下。 优点: 1、高可靠性,主从实时备份,有效解决单节点数据丢失问题。 2、可做读写分离,从库分担读操作,缓解主库压力 缺点:主库异常,需要手动主从切换 **哨兵模式** 哨兵模式:是基于主从模式做的一定变化,它能够为Redis提供了高可用性。其实,哨兵模式的核心还是主从复制。只不过相对于主从模式在主节点宕机导致不可写的情况下,多了一个竞选机制——从所有的从节点竞选出新的主节点。竞选机制的实现,是依赖于在系统中启动一个sentinel(哨兵)进程。哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。 哨兵有两个作用 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。 sentinel特点: 监控:它会监听主服务器和从服务器之间是否在正常工作。 通知:它能够通过API告诉系统管理员或者程序,集群中某个实例出了问题。 故障转移:它在主节点出了问题的情况下,会在所有的从节点中竞选出一个节点,并将其作为新的主节点。 提供主服务器地址:它还能够向使用者提供当前主节点的地址。这在故障转移后,使用者不用做任何修改就可以知道当前主节点地址。 **Redis集群(cluster)** Redis 集群是一个提供在多个Redis间节点间共享数据的程序集。 Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误. Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令. Redis 集群的优势: 自动分割数据到不同的节点上。 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。 Redis 集群的数据分片Redis集群没有使用一致性hash, 而是引入了 哈希槽的概念. Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点 Redis 集群的主从复制模型 为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品. 在我们例子中具有A,B,C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用. 然而如果在集群创建的时候(或者过一段时间)我们为每个节点添加一个从节点A1,B1,C1,那么整个集群便有三个master节点和三个slave节点组成,这样在节点B失败后,集群便会选举B1为新的主节点继续服务,整个集群便不会因为槽找不到而不可用了 不过当B和B1 都失败后,集群是不可用的. Redis 一致性保证 Redis 并不能保证数据的强一致性. 这意味这在实际中集群在特定的条件下可能会丢失写操作 ### [8.Redis 和 Memcached 有啥区别?](https://adjava.netlify.app/#/./docs/high-concurrency/redis-single-thread-model?id=redis-和-memcached-有啥区别?) #### [Redis 支持复杂的数据结构](https://adjava.netlify.app/#/./docs/high-concurrency/redis-single-thread-model?id=redis-支持复杂的数据结构) Redis 相比 Memcached 来说,拥有[更多的数据结构](https://adjava.netlify.app/#/docs/high-concurrency/redis-data-types),能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, Redis 会是不错的选择。 #### [Redis 原生支持集群模式](https://adjava.netlify.app/#/./docs/high-concurrency/redis-single-thread-model?id=redis-原生支持集群模式) 在 Redis3.x 版本中,便能支持 cluster 模式,而 Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。 #### [性能对比](https://adjava.netlify.app/#/./docs/high-concurrency/redis-single-thread-model?id=性能对比) 由于 Redis 只使用**单核**,而 Memcached 可以使用**多核**,所以平均每一个核上 Redis 在存储小数据时比 Memcached 性能更高。而在 100k 以上的数据中,Memcached 性能要高于 Redis。虽然 Redis 最近也在存储大数据的性能上进行优化,但是比起 Memcached,还是稍有逊色。 ### [9.Redis 的线程模型](https://adjava.netlify.app/#/./docs/high-concurrency/redis-single-thread-model?id=redis-的线程模型) Redis 内部使用文件事件处理器 `file event handler` ,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。 文件事件处理器的结构包含 4 个部分: - 多个 socket - IO 多路复用程序 - 文件事件分派器 - 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) 多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。 来看客户端与 Redis 的一次通信过程: ![Redis-single-thread-model](https://adjava.netlify.app/docs/high-concurrency/images/redis-single-thread-model.png) 要明白,通信是通过 socket 来完成的,不懂的同学可以先去看一看 socket 网络编程。 首先,Redis 服务端进程初始化的时候,会将 server socket 的 `AE_READABLE` 事件与连接应答处理器关联。 客户端 socket01 向 Redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个 `AE_READABLE` 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。文件事件分派器从队列中获取 socket,交给**连接应答处理器**。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 `AE_READABLE` 事件与命令请求处理器关联。 假设此时客户端发送了一个 `set key value` 请求,此时 Redis 中的 socket01 会产生 `AE_READABLE` 事件,IO 多路复用程序将 socket01 压入队列,此时事件分派器从队列中获取到 socket01 产生的 `AE_READABLE` 事件,由于前面 socket01 的 `AE_READABLE` 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 `key value` 并在自己内存中完成 `key value` 的设置。操作完成后,它会将 socket01 的 `AE_WRITABLE` 事件与命令回复处理器关联。 如果此时客户端准备好接收返回结果了,那么 Redis 中的 socket01 会产生一个 `AE_WRITABLE` 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 `ok` ,之后解除 socket01 的 `AE_WRITABLE` 事件与命令回复处理器的关联。 这样便完成了一次通信。关于 Redis 的一次通信过程,推荐读者阅读《[Redis 设计与实现——黄健宏](https://github.com/doocs/technical-books#database)》进行系统学习。 ## 7.7 Netty ### 1.netty是什么 Netty是最流行的NIO框架,它的健壮性、功能、性能、可定制性和可扩展性在同类框架都是首屈一指的。它已经得到成百上千的商业/商用项目验证,如Hadoop的RPC框架Avro、RocketMQ以及主流的分布式通信框架Dubbox等等。 Netty是基于Java NIO client-server的网络应用框架,使用Netty可以快速开发网络应用,例如服务器和客户端协议。 服务端 ①创建两个NIO线程组,一个专门用于网络事件处理(接受客户端的连接),另一个则进行网络通信的读写。 ②创建一个ServerBootStrap对象,配置Netty的一系列参数,例如接受传出数据的缓存大小等。 ③创建一个用于实际处理数据的类ChannelInitializer,进行初始化的准备工作,比如设置接受传出数据的字符集、格式以及实际处理数据的接口。 ④绑定端口,执行同步阻塞方法等待服务器端启动即可。 ## 7.8 Shiro ### 1.Shiro是什么 Apache Shiro 是 Java 的一个安全框架。使用 shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE环境,也可以用在 JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。 ### 2.主要组件 Subject: 即“当前操作用户”。但是,在 Shiro 中,Subject 这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是 Shiro 的“用户”概念 Subject 代表了当前用户的安全操作,SecurityManager 则管理所有用户的安全操作。 SecurityManager: 它是 Shiro 框架的核心,典型的 Facade 模式,Shiro 通过 SecurityManager 来管理内部组件实例,并通过它来提供安全管理的各种服务。 Realm: Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。 Authenticator: 对“Who are you ?”进行核实。通常涉及用户名和密码。 这个组件负责收集 principals 和 credentials,并将它们提交给应用系统。如果提交的 credentials 跟应用系统中提供的 credentials 吻合,就能够继续访问,否则需要重新提交 principals 和credentials, 或者直接终止访问。 Authorizer: 身份份验证通过后,由这个组件对登录人员进行访问控制的筛查,比如“who can do what”, 或者“who can do which actions”。 Shiro 采用“基于 Realm”的方法,即用户(又称Subject)、 用户组、角 色和permission 的聚合体。 Session Manager: 这个组件保证了异构客户端的访问,配置简单。它是基于 POJO/J2SE 的,不跟任何的客户端或者协议绑定。 ### 3.Shiro 运行原理 ![img](http://cdn.processon.com/5e6cc7fde4b0dab55408cace?e=1584190989&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:ai3SAWD3JhuialyxP13xZvF_aJA=) 1、Application Code:应用程序代码,就是我们自己的编码,如果在程序中需要进 行权限控制,需要调用 Subject 的 API。 2、Subject:主体,代表的了当前用户。所有的 Subject 都绑定到 SecurityManager, 与 Subject 的所有 交互都会委托给 SecurityManager,可以将 Subject 当成一个 门面,而真正执行者是 SecurityManager 。 3、SecurityManage:安全管理器,所有与安全有关的操作都会与 SecurityManager 交互,并且它管理所有 的 Subject 。 4、Realm:域 shiro 是从 Realm 来获取安全数据(用户,角色,权限)。就是说 SecurityManager 要验证用户身份, 那么它需要从 Realm 获取相应的用户进行比较以确定用户 身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否 能进行操作; 可以把 Realm 看成 DataSource,即安全数据源. ## 7.9 消息中间件 ### 1.MQ消息中间件在分布式系统中的作用 1.分布式服务间的异步通信 2.对Dubbo服务间的调用进行解耦 3.通过异步提高程序响应速度 异步通讯、解耦、并发缓冲 消息中间件使用的典型场景优四个 1.典型的异步处理 2.应用解耦 3.流量削锋 4.消息通讯四个场景 ### 2.ActiveMQ jms即Java消息服务(Java Message Service)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。 JMS(Java Messaging Service)是Java平台上有关面向消息中间件的技术规范,它便于消息系统中的Java应用程序进行消息交换,并且通过提供标准的产生、发送、接收消息的接口简化企业应用的开发,翻译为Java消息服务。  JMS有以下元素组成 1.1JMS提供者 连接面向消息中间件的,JMS接口的一个实现。提供者可以是Java平台的JMS实现,也可以是非Java平台的面向消息中间件的适配器。 1.2JMS客户  生产或消费消息的基于Java的应用程序或对象。 1.3JMS生产者   创建并发送消息的JMS客户。 1.4JMS消费者  接收消息的JMS客户。 1.5JMS消息  包括可以在JMS客户之间传递的数据的对象 1.6JMS队列  一个容纳那些被发送的等待阅读的消息的区域。队列暗示,这些消息将按照顺序发送。一旦一个消息被阅读,该消息将被从队列中移走。 1.7JMS主题  一种支持发送消息给多个订阅者的机制。 JMS模型 Point-to-Point(P2P)或队列模型和Publish/Subscribe(Pub/Sub),即点对点和发布订阅模型。 消息事务 消息事务是在生产者producer到broker或broker到consumer过程中同一个session中发生的,保证几条消息在发送过程中的原子性。(Broker:消息队列核心,相当于一个控制中心,负责路由消息、保存订阅和连接、消息确认和控制事务) 在支持事务的session中,producer发送message时在message中带有transactionID。broker收到message后判断是否有transactionID,如果有就把message保存在transaction store中,等待commit或者rollback消息。 ### 3.Kafka Apache的Kafka是一个分布式流平台(a distributed streaming platform)。 一个流处理平台应该具有三个关键能力: 它可以让你发布和订阅记录流。在这方面,它类似于一个消息队列或企业消息系统。 它可以让你持久化收到的记录流,从而具有容错能力。 它可以让你处理收到的记录流。 Kafka应用场景: 建立实时流数据管道从而能够可靠地在系统或应用程序之间的共享数据 构建实时流应用程序,能够变换或者对数据进行相应的处理。 #### 1.基本介绍 **是什么** Apache的Kafka是一个分布式流平台(a distributed streaming platform)。 一个流处理平台应该具有三个关键能力: 它可以让你发布和订阅记录流。在这方面,它类似于一个消息队列或企业消息系统。 它可以让你持久化收到的记录流,从而具有容错能力。 它可以让你处理收到的记录流。 **应用场景** 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。 消息系统:解耦和生产者和消费者、缓存消息等。 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。 流式处理:比如spark streaming和storm **特点** 1.高吞吐率 在廉价的商用机器上单机可支持每秒100万条消息的读写 2.消息持久化 所有消息均被持久化到磁盘,无消息丢失,支持消息重放 3.完全分布式 Producer,Broker,Consumer均支持水平扩展 4.同时适应在线流处理和离线批处理 #### 2.基本概念 **Kafka文件存储机制** Broker Kafka节点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群。 Topic Topic是用于存储消息的逻辑概念,可以看作一个消息集合。 每个topic可以有多个生产者向其推送消息,也可以有任意多个消费者消费其中的消息。 每个topic可以划分多个分区(每个Topic至少有一个分区),同一topic下的不同分区包含的消息是不同的 Partition topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列 Segment partition物理上由多个segment组成,每个Segment存着message信息 Producer 生产message发送到topic Consumer 订阅topic消费message, consumer作为一个线程来消费 Consumer Group 一个Consumer Group包含多个consumer, 这个是预先在配置文件中配置好的。各个consumer(consumer 线程)可以组成一个组(Consumer group ),partition中的每个message只能被组(Consumer group ) 中的一个consumer(consumer 线程 )消费 #### 3.消息可靠性 **消息发送可靠性** 生产者发送消息到broker,有三种确认方式(request.required.acks) acks = 0: producer不会等待broker(leader)发送ack 。因为发送消息网络超时或broker crash(1.Partition的Leader还没有commit消息 Leader与Follower数据不同步),既有可能丢失也可能会重发。 acks = 1: 当leader接收到消息之后发送ack,意味若 Leader 在收到消息并把它写入到分区数据文件(不一定同步到磁盘上)时会返回确认或错误响应,丢会重发,丢的概率很小,意味若 Leader 在收到消息并把它写入到分区数据文件(不一定同步到磁盘上)时会返回确认或错误响应 acks = -1: 当所有的follower都同步消息成功后发送ack. 丢失消息可能性比较低。 **消息存储的可靠性** 每一条消息被发送到broker中,会根据partition规则选择被存储到哪一个partition。如果partition规则设置的合理,所有消息可以均匀分布到不同的partition里,这样就实现了水平扩展。 在创建topic时可以指定这个topic对应的partition的数量。在发送一条消息时,可以指定这条消息的key,producer根据这个key和partition机制来判断这个消息发送到哪个partition。 kafka的高可靠性的保障来自于另一个叫副本(replication)策略,通过设置副本的相关参数,可以使kafka在性能和可靠性之间做不同的切换。 **副本机制** kafka中,replication策略是基于partition,而不是topic; kafka将每个partition数据复制到多个server上,任何一个partition有一个leader和多个follower(可以没有); 备份的个数可以通过broker配置文件来设定。leader处理所有的read-write请求,follower需要和leader保持同步.Follower就像一个"consumer",消费消息并保存在本地日志中; leader负责跟踪所有的follower状态,如果follower"落后"太多或者失效,leader将会把它从replicas同步列表中删除. 当所有的follower都将一条消息保存成功,此消息才被认为是"committed",那么此时consumer才能消费它,这种同步策略,就要求follower和leader之间必须具有良好的网络环境.即使只有一个replicas实例存活,仍然可以保证消息的正常发送和接收,只要zookeeper集群存活即可. 选择follower时需要兼顾一个问题,就是新leader server上所已经承载的partition leader的个数,如果一个server上有过多的partition leader,意味着此server将承受着更多的IO压力.在选举新leader,需要考虑到"负载均衡",partition leader较少的broker将会更有可能成为新的leader. **ISRISR(副本同步队列)** 维护的是有资格的follower节点 副本的所有节点都必须要和zookeeper保持连接状态 副本的最后一条消息的offset和leader副本的最后一条消息的offset之间的差值不能超过指定的阀值,这个阀值是可以设置的(replica.lag.max.messages) #### 4.高可用设计 **数据备份** Kafka尽量将所有的Partition均匀分配到整个集群上。一个典型的部署方式是一个Topic的Partition数量大于Broker的数量。同时为了提高Kafka的容错能力,也需要将同一个Partition的Replica尽量分散到不同的机器。 **Producer在发布消息**  Producer在发布消息到某个Partition时,先找到该 Partition 的Leader,将该消息发送到该Partition的Leader。Leader会将该消息写入其本地Log。每个Follower都从Leader pull数据。这种方式上,Follower存储的数据顺序与Leader保持一致。Follower在收到该消息并写入其Log后,向Leader发送ACK。一旦Leader收到了ISR中的所有Replica的ACK,该消息就被认为已经commit了,Leader将增加HW并且向Producer发送ACK。 为了提高性能,每个Follower在接收到数据后就立马向Leader发送ACK,而非等到数据写入Log中。因此,对于已经commit的消息,Kafka只能保证它被存于多个Replica的内存中,而不能保证它们被持久化到磁盘中,也就不能完全保证异常发生后该条消息一定能被Consumer消费。但考虑到这种场景非常少见,可以认为这种方式在性能和数据持久化上做了一个比较好的平衡。 **ACK前需要保证有多少个备份** ISR有多少的Follower,就有多少个 **如何处理所有Replica都不工作** 在ISR中至少有一个follower时,Kafka可以确保已经commit的数据不丢失,但如果某个Partition的所有Replica都宕机了,就无法保证数据不丢失了。这种情况下有两种可行的方案: 等待ISR中的任一个Replica“活”过来,并且选它作为Leader 选择第一个“活”过来的Replica(不一定是ISR中的)作为Leader **领导选举Leader Election算法** **Kafka “各自为政” Leader Eclection** 每个Partition的多个Replica 同时竞争Leader; 优点: 实现简单; 缺点: Herd Effect;羊群效应 Zookepper负载过量; Latency(延时)较大; **Kafka基于 Controller 的 Leader Election** 基于Controller的Leader Election 整个集群汇中选举出一个Broker作为Controller; Controller为所有的Topic的所有的Partition指定Leader以及Follower; controller会将Leader的改变直接通过RPC的方式(比Zookeeper Queue的方式更高效)通知需为此作出响应的Broker。同时controller也负责增删Topic以及Replica的重新分配。 优点: 极大缓解了Herd Effect问题; 减轻了Zookepper负载; Controller 与Leader以及Follower间通过RPC通信,高效且实时; 缺点: 引入Controller增加了复杂度; 需要考虑Controller的Failover; #### 5.消费者 Kafka提供了两套consumer api,分为high-level api和sample-api High level api和Low level api是针对consumer而言的,和producer无关。 **high-level api** High-level API封装了对集群中一系列broker的访问,可以透明的消费一个topic。它自己维持了已消费消息的状态(High Level Consumer将从某个Partition读取的最后一条 消息的offset存于Zookeeper中(从0.8.2开始同时支持将 offset存于Zookeeper中和专用的Kafka Topic中),即每次消费的都是下一个消息,如果使用了High level api, 每个message只能被读一次,一旦读了这条message之后,无论我consumer的处理是否ok。High level api的另外一个线程会自动的把offiste+1同步到zookeeper上。如果consumer读取数据出了问题,offsite也会在zookeeper上同步。因此,如果consumer处理失败了,会继续执行下一条。这往往是不对的行为。因此,Best Practice是一旦consumer处理失败,直接让整个conusmer group抛Exception终止,但是最后读的这一条数据是丢失了,因为在zookeeper里面的offsite已经+1了。等再次启动conusmer group的时候,已经从下一条开始读取处理了。 **Sample-api** Sample-api 是一个底层的API,它维持了一个和单一broker的连接,并且这个API是完全无状态的,每次请求都需要指定offset值,因此,这套API也是最灵活的。 在kafka中,当前读到哪条消息的offset值是由consumer来维护的,因此,consumer可以自己决定如何读取kafka中的数据。比如,consumer可以通过重设offset值来重新消费已消费过的数据。不管有没有被消费,kafka会保存数据一段时间,这个时间周期是可配置的,只有到了过期时间,kafka才会删除这些数据。 **Consumer group** 1. 允许consumer group(包含多个consumer,如一个集群同时消费)对一个topic进行消费,不同的consumer group之间独立消费。 2. 为了对减小一个consumer group中不同consumer之间的分布式协调开销,指定partition为最小的并行消费单位,即一个group内的consumer只能消费不同的partition。 Consumer与Partition的关系: 如果consumer比partition多,是浪费,因为kafka的设计是在一个partition上是不允许并发的,所以consumer数不要大于partition数 如果consumer比partition少,一个consumer会对应于多个partitions,这里主要合理分配consumer数和partition数,否则会导致partition里面的数据被取的不均匀 如果consumer从多个partition读到数据,不保证数据间的顺序性,kafka只保证在一个partition上数据是有序的,但多个partition,根据你读的顺序会有不同 增减consumer,broker,partition会导致rebalance,所以rebalance后consumer对应的partition会发生变化 High-level接口中获取不到数据的时候是会block的 #### 6.kafka的高吞吐量的因素 1.顺序写的方式存储数据 2.批量发送 在异步发送模式中。kafka允许进行批量发送,也就是先讲消息缓存到内存中,然后一次请求批量发送出去。这样减少了磁盘频繁io以及网络IO造成的性能瓶颈 batch.size 每批次发送的数据大小 linger.ms 间隔时间 3.零拷贝 跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存的直接映射,数据不再复制到“用户态缓冲区”。 消息从发送到落地保存,broker维护的消息日志本身就是文件目录,每个文件都是二进制保存,生产者和消费者使用相同的格式来处理。在消费者获取消息时,服务器先从硬盘读取数据到内核空间,然后把内核空间的数据原封不懂的通过socket发送给消费者,不要在传到用户空间。 ![img](http://cdn.processon.com/5e6c85d5e4b0dab55408520b?e=1584174053&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:SoqpNZpBiyBOlIZQl0W9u_mMUTU=) 4.数据压缩 Kafka还支持对消息集合进行压缩,Producer可以通过GZIP或Snappy格式对消息集合进行压缩。 #### 7.日志保留策略 kafka有两种“保留策略”: 根据消息保留的时间,当消息在kafka中保存的时间超过了指定时间,就可以被删除; 根据topic存储的数据大小,当topic所占的日志文件大小大于一个阀值,则可以开始删除最旧的消息 ### 4.RocketMQ RocketMQ是一款分布式消息中间件,最初是由阿里巴巴消息中间件团队研发并大规模应用于生产系统,满足线上海量消息堆积的需求 RocketMQ具有以下特点: 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式特点。 Producer、Consumer、队列都可以分布式。 Producer向一些队列轮流发送消息,队列集合称为Topic,Consumer如果做广播消费,则一个consumer实例消费这个Topic对应的所有队列,如果做集群消费,则多个Consumer实例平均消费这个topic对应的队列集合。 能够保证严格的消息顺序 提供丰富的消息拉取模式 高效的订阅者水平扩展能力 实时的消息订阅机制 亿级消息堆积能力 较少的依赖 #### 1.RocketMQ整体架构 ##### 1.RocketMQ逻辑部署结构 ![img](http://cdn.processon.com/5f4b53831e0853452d428485?e=1598775699&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:6CCPvmir5m0oP2yXJOz51r9khB4=) Producer Group 用来表示一个发送消息应用,一个Producer Group下包含多个Producer实例,可以是多台机器,也可以是一台机器的多个进程,或者一个进程的多个Producer对象。一个Producer Group可以发送多个Topic消息,Producer Group作用如下: 标识一类Producer 可以通过运维工具查询这个发送消息应用下有多个Producer实例 发送分布式事务消息时,如果Producer中途意外宕机,Broker会主动回调Producer Group内的任意一台机器来确认事务状态。 Consumer Group 用来表示一个消费消息应用,一个Consumer Group下包含多个Consumer实例,可以是多台机器,也可以是多个进程,或者是一个进程的多个Consumer对象。一个Consumer Group下的多个Consumer以均摊方式消费消息,如果设置为广播方式,那么这个Consumer Group下的每个实例都消费全量数据。 ##### 2.RocketMQ数据存储结构 ![img](http://cdn.processon.com/5f4b53a5079129356ec5a434?e=1598775733&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:iGZ_oOItu0TjmXXrseyy7yhPSS4=) RocketMQ采取了一种数据与索引分离的存储方法。有效降低文件资源、IO资源,内存资源的损耗。即便是阿里这种海量数据,高并发场景也能够有效降低端到端延迟,并具备较强的横向扩展能力。 ##### 3.RocketMQ物理部署结构 ![img](http://cdn.processon.com/5f4b5374e0b34d1abc6fa264?e=1598775684&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:7oN2-D8G8cmjaW9FgV3cerXceIg=) RocketMQ的部署结构有以下特点: Name Server是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。 Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。 Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。 Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。 #### 2.架构设计 ##### 1.部署架构 在部署Rocket我们主要要注意以下几点: NameServer :NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。我们最后在项目中(Producer或Consumer)使用的一般也都是连接NameServer服务进行消息的生产和消费。 Broker:Broker的部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId 来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。 注意:当前RocketMQ版本在部署架构上支持一Master多Slave,但只有BrokerId=1的从服务器才会参与消息的读负载。 Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer对于RocketMQ来说完全无状态,可集群部署。 Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Master Broker和Slave Broker建立长连接,且定时向Master Broker、Slave Broker发送心跳。 Consumer既可以从Master Broker订阅消息,也可以从Slave Broker订阅消息,消费者在向Master Broker拉取消息时,Master Broker服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还是Slave拉取。 ![img](http://cdn.processon.com/5f4b580cf346fb2e295be803?e=1598776860&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:kG08IrBCzmzsYTd5YdNso19VSu8=) ##### 2.技术架构 Producer: 发布(生产)消息的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。 Consumer: 消费消息的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。 NameServer: NameServer是一个非常简单的Topic路由注册中心,其角色作用类似Dubbo中的Zookeeper,支持Broker的动态注册与发现。主要负责两个功能: Broker管理:NameServer接受Broker集群的注册信息并且保存下来作为消息路由信息的基本数据。然后提供心跳检测机制,检查Broker的健康/存活状态。 消息路由管理:每个NameServer将保存关于Broker集群的整个路由信息以及用于客户端查询的队列信息。然后Producer和Consumer通过NameServer就可以知道整个Broker的集群的路由信息,从而进行消息的投递和消费。NameServer通常也使用集群的方式部署,各个NameServer实例之间不进行信息的通讯和交换。Broker会向NameServer集群内的每个实例注册自己的路由信息,所以每个NameServer实例上面都保存有一份完整的路由信息。当某一台NameServer实例因为某种原因下线了,Broker仍然可以向其他NameServer实例同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。 BrokerServer: Broker主要负责消息的存储、投递和查询以及保证服务的高可用,为了实现这些功能,Broker还包括了一下几个重要子模块: ![img](http://cdn.processon.com/5f4b57dd7d9c082a6ba96843?e=1598776814&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:iid93m2VNaPXLGJAf1rHIkpdcEk=) ##### 3.RocketMQ集群工作流程 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心。 Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以配置Broker在收到发送的消息时自动创建Topic。 Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。 Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。 **NameServer保证AP** #### 3.顺序消息 生成消息的顺序性 原理:同一类消息发送到相同的对列即可,为了保证先发送的消息先存储到对列,必须使用同步发送 顺序消费 原理:同一个消息对列只允许消费者中的一个消费线程拉取消息,顺序消费消费线程请求到broker时会先申请锁 #### 4.消息幂等性 rocketMq不保证消息不被重复消费, 要实现消息幂等性可通过分布式锁来实现 消费消息采用的是at-least-once RocketMQ消息至少一次(At least Once)投递和消费 至少一次(At least Once)指每个消息必须投递一次。Consumer先Pull消息到本地,消费完成后,才向服务器返回ack,如果没有消费一定不会ack消息,所以RocketMQ可以很好的支持此特性。 #### 5.高性能设计 ##### 1.顺序写盘 RocketMQ 在持久化的设计上,采取的是「消息顺序写、随机读的策略」,利用磁盘顺序写的速度,让磁盘的写速度不会成为系统的瓶颈。并且采用 MMPP 这种“零拷贝”技术,提高消息存盘和网络发送的速度。极力满足 RocketMQ 的高性能、高可靠要求。 RocketMQ 持久化机制中,涉及到了三个角色: 「CommitLog」:消息真正的存储文件,所有消息都存储在 CommitLog 文件中。「ConsumeQueue」:消息消费逻辑队列,类似数据库的索引文件。「IndexFile」:消息索引文件,主要存储消息 Key 与 offset 对应关系,提升消息检索速度。 咱们逐一聊聊吧,CommitLog 文件是存放消息数据的地方,所有的消息都将存入到 CommitLog 文件中。生产者将消息发送到 RocketMQ 的 Broker 后,Broker 服务器会将「消息顺序写入到 CommitLog 文件中」,这也就是 RocketMQ 高性能的原因,因为我们知道磁盘顺序写特别快,RocketMQ 充分利用了这一点,极大的提高消息写入效率。 但是消费者消费消息的时候,可能就会遇到麻烦,每一个消费者只能订阅一个主题,消费者关心的是订阅主题下的所有消息,但是同一主题的消息在 CommitLog 文件中可能是不连续的,那么「消费者消费消息的时候,需要将 CommitLog 文件加载到内存中遍历查找订阅主题下的消息,频繁的 IO 操作,性能就会急速下降」。 为了解决这个问题,RocketMQ 引入了 Consumequeue 文件。「Consumequeue 文件可以看作是索引文件,类似于 MySQL 中的二级索引」。在存放了同一主题下的所有消息,消费者消费的时候只需要去对应的 Consumequeue 组中取消息即可。Consumequeue 文件不会存储消息的全量信息,了解 MySQL 索引的话,应该好理解这里,具体存储的字段,我在上图已经标注。这样做可以带来以下两个好处: 由于 Consumequeue 文件内容小,可以尽可能的保证 Consumequeue 文件全部读入到内存,提高消费效率。Consumequeue 文件也是会持久化的,不存全量信息可以节约磁盘空间。 「IndexFile」 是 RocketMQ 为消息订阅构建的索引文件,用来提高根据主题与消息队列检索消息的速度,这个就不细说了。 ##### 2.数据零拷贝 linux有两个上下文(内核态、用户态), 传统的将一个file读取并发送出去会经历4个过程。  read时:   1. 将文件从磁盘copy到kernel(内核)态   2. cpu将kernrl态的数据copy到user(用户)态  write时:   3. user态的内容会copy到kernel态的socket的buffer中   4. 将kernel中buffer的数据copy到网卡中传送  我们可以发现2、3完全是多余的步骤,而且上下文之间的切换是很耗性能的。     ZeroCopy:内核直接把磁盘的数据传输到socket,而不是通过应用程序去传输。减少了不必要的内核缓冲区和用户缓冲区间的拷贝,从而提升了性能。   零拷贝技术有mmap及sendfile;sendfile大文件传输快,mmap小文件传输快。MMQ发送的消息通常都很小,rocketmq就是以mmap+write方式实现的。像kafka、netty都采用了零拷贝技术。 ##### 3.消息实时投递 Rocketmq消费模型(实时性) 常见的数据同步方式有这几种:   push:producer发送消息后,broker马上把消息投递给consumer。这种方式好在实时性比较高,但是会增加broker的负载;而且消费端能力不同,如果push推送过快,消费端会出现很多问题。   pull:producer发送消息后,broker什么也不做,等着consumer自己来读取。它的优点在于主动权在消费者端,可控性好;但是间隔时间不好设置,间隔太短浪费资源,间隔太长又会消费不及时。   长轮询:当consumer过来请求时,broker会保持当前连接一段时间 默认15s,如果这段时间内有消息到达,则立刻返回给consumer;15s没消息的话则返回空然后重新请求。这种方式的缺点就是服务端要保存consumer状态,客户端过多会一直占用资源。 RocketMQ默认是采用pushConsumer方式消费的,从概念上来说是推送给消费者,它的本质是pull+长轮询。这样既通过长轮询达到了push的实时性,又有了pull的可控性。系统收到消息后会自动处理消息和offset(消息偏移量),如果期间有新的consumer加入会自动做负载均衡(集群模式下offset存在broker中; 广播模式下offset存在consumer里)。当然我们也可以设置为pullConsumer模式,这样灵活性会提高,但是代码却会很复杂,需要手动维护offset,消息存储和状态。   * offset:简单粗暴的理解就是数组下标。message queue是无限长的数组,每次消息进来就会涨1,下标就是offset。consumer可以通过指定offse位置开始读取数据。queue的maxOffset是消息的最大offset,不是最新消息的offset 而是最新消息的offset+1,minOffset则是现存的最小offset。 #### 6.消息发送的高可用 在消息发送时可能会遇到网络问题、Broker宕机等情况, 而NameSever检测Broker是有延迟的,虽然NameSever每间隔10秒会扫描所有Broker信息, 但要Broker的最后心跳时间超过120秒以上才认为该Broker不可用, 所以Producer不能及时感知Broker下线。如果在这期间消息一直发送失败, 那么消息发送失败率会很高,这在业务上是无法接受的。这里大家可能会有一个疑问, 为什么NameServer不及时检查Broker和通知Producer?这是因为那样做会使网络通信和架构设计变得非常复杂, 而NameServer的设计初衷就是尽可能简单, 所以这块的高可用方案在Producer中来实现。RocketMQ采用了一些发送端的高可用方案,来解决发送失败的问题,其中最重要的两个设计是重试机制与故障延迟机制。 ##### 1.消息发送的重试机制 消息发送异常是会再次发送,默认最多重试三次,重试机制仅支持同步发送方式,不支持异步和单向发送根据发送失败的异常类型处理策略略有不同,如果是网络异常和客户端异常会重试,而broker异常MQBrokerExcption和线程中断异常不会重试,切抛异常 ##### 2.故障规避机制 在介绍NameServer时提到,NameServer为了简化和客户端通信,发现Broker故障时并不会立即通知客户端。故障规避机制用来解决当Broker出现故障,Producer不能及时感知而导致消息发送失败的问题。默认是不开启的,如果在开启的情况下,消息发送失败的时候会将失败的Broker暂时排除在队列选择列表外。规避时间是衰减的,如果Broker一直不可用,会被NameServer检测到并在Producer更新路由信息时进行剔除。 在选择查找路由时,选择消息队列的关键步骤如下: 先按轮询算法选择一个消息队列 从故障列表判断该消息队列是否可用 判断消息队列是否可用有两个步骤: 判断其是否在故障列表中,不在故障列表中代表可用。 在故障列表faultItemTable中还需要判断当前时间是否大于等于故障规避的开始时间startTimestamp,使用这个时间判断是因为通常故障时间是有限制的,Broker宕机之后会有相关运维去恢复。 这部分重点在于故障机器FaultItem在什么场景下进入故障列表faultItemTable中,消息发送失败时就可能使机器故障了。 #### 7.消息存储的高可用 ##### 1.消息持久化(同步刷盘、异步刷盘) RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时候,有两种 写磁盘方式:   1)异步刷盘方式(默认):在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入 优点:性能高 缺点:Master宕机,磁盘损坏的情况下,会丢失少量的消息, 导致MQ的消息状态和生产者/消费者的消息状态不一致   2)同步刷盘方式:在返回应用写成功状态前,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,给应用返回消息写成功的状态。 优点:可以保持MQ的消息状态和生产者/消费者的消息状态一致 缺点:性能比异步的低 ##### 2.主从复制 RocketMQ为了提高消息消费的高可用性,避免Broker发生单点故障引起存储在Broker上的消息无法及时消费,同时避免单个机器上硬盘损坏出现消息数据丢失。RocketMQ采用Broker数据主从复制机制,当消息发送到Master服务器后会将消息同步到Slave服务器,如果Master服务器宕机,消息消费者还可以继续从Slave拉取消息。 消息从Master服务器复制到Slave服务器上,有两种复制方式:同步复制SYNC_MASTER和异步复制ASYNC_MASTER。通过配置文件${ROCKETMQ_HOME}/conf/broker.conf里面的brokerRole参数进行设置。 同步复制:Master服务器和Slave服务器都写成功后才返回给客户端写成功的状态。优点是如果Master服务器出现故障,Slave服务器上有全部数据的备份,很容易恢复到Master服务器。缺点是由于多了一个同步等待的步骤,会增加数据写入延迟,并且降低系统的吞吐量。 异步复制:仅Master服务器写成功即可返回给客户端写成功的状态。有点刚好是同步复制的缺点,由于没有那一次同步等待的步骤,服务器的延迟比较低且吞吐量较高。缺点显而易见,如果Master服务器出现故障,有些数据因为没有被写入Slave服务器,未同步的数据有可能丢失。 在实际应用中需要结合业务场景,合理设置刷盘方式和主从复制方式。不建议使用SYNC_FLUSH同步刷盘方式,因为它会频繁地触发写磁盘操作,性能下降明显。高性能是RocketMQ的一个明显特点,因此放弃性能是不合适的选择。通常可以把Master和Slave设置成异步刷盘、同步复制,这样即使有一台服务器出现故障,仍然可以保证数据不丢失。 ##### 3.读写分离机制 #### 8.消息消费的高可用 ##### 1.消费重试机制 实际业务场景中无法避免消费消息失败的情况,消费失败可能是因为业务处理中调用远程服务网络问题失败,不代表消息一定不能被消费,通过重试可以解决。介绍RocketMQ的消费重试机制之前,需要先了解一下重试队列和死信队列。 重试队列:在Consumer由于业务异常导致消费消息失败时,将消费失败的消息重新发送给Broker保存在重试队列,这样设计的原因是不能影响整体消费进度又必须防止消费失败的消息丢失。重试队列的消息存在一个单独的Topic中,不在原消息的Topic中,Consumer自动订阅该Topic。重试队列的Topic名称格式为"%RETRY%+consumerGroup",每个业务Topic都会有多个ConsumerGroup,每个ConsumerGroup消费失败的情况都不一样,因此各对应一个重试队列的Topic。 死信队列:由于业务逻辑Bug等原因,导致Consumer对部分消息长时间消费重试一直失败,为了保证这部分消息不丢失,同时不能阻塞其它能重试消费成功的消息,超过最大重试消费次数之后的消息会进入私信队列。消息进入死信队列之后就不再自动消费,需要人工干预处理。死信队列也存在一个单独的Topic中,名称格式为"%DLQ%+consumerGroup", 原理和重试队列一致。 通常故障恢复需要一定的时间,如果不间断地重试,重试又失败的情况会占用并浪费资源,所以RocketMQ的消费重试机制采用时间衰减的方式,使用了自身定时消费的能力。首次在10秒后重试消费,如果消费成功则不再重试,如果消费失败则继续重试消费,第二次在30秒后重试消费。以此类推,每次重试的时间间隔都会加长,直到超出最大重试次数(默认为16次),则进入死信队列不再重试。 重试消费过程中的间隔时间使用了定时消息,重试的消息数据并非直接写入重试队列,而是先写入定时消息队列,再通过定时消息的功能转发到重试队列。 RocketMQ支持定时消息(也称延迟消息),延迟消息是指消息发送之后,等待指定的延迟时间后再进行消费。除了支持消息重试机制,延迟消息也适用于一些处理异步任务的场景,例如调用某个服务,调用结果需要异步在一分钟内返回,此时就可以发送一个延迟消息,延迟时间为1分钟,等1分钟后收到该消息去查询上次的调用结果是否返回。 RocketMQ不支持任意时间精确的延迟消息,仅支持1s、5s、10s、30s、1min、2min、3min、4min、5min、6min、7min、8min、9min、10min、20min、30min、1h、2h。 ##### 2.消息ACK机制来保证 在实际业务场景中,业务应用在消费消息的过程中偶尔会出现一些异常的情况,例如程序发布导致的重启,或者网络突然出现问题,此时正在进行业务处理的消息可能消费完了,也可能业务逻辑执行到一半没有消费完,那么如何去识别这些情况呢?这就需要消息的ACK机制。 广播模式的消费进度保存在客户端本地,集群模式的消费进度保存在Broker上。集群模式中RocketMQ采用ACK机制确保消息一定被消费。在消息投递过程中,不是消息从Broker发送到Consumer就算消费成功了,需要Consumer明确给Broker返回消费成功状态才算。如果从Broker发送到Consumer后,已经完成了业务处理,但在给Broker返回消费成功状态之前,Consumer发生宕机或者断电、断网等情况,Broker未收到返回,则不会保存消费进度。 Consumer重启之后,消息会重新投递,此时也会出现重复消费的场景,这时消息的幂等性需要业务自行保证。 ##### 3.集群管理的高可用 集群管理的高可用主要体现在NameServer的设计上, 当部分NameServer节点宕机时不会有什么糟糕的影响, 只剩一个NameServer节点RocketMQ集群也能正常运行, 即使NameServer全部宕机, 也不影响己经运行的Broker、Producer和Consumer。 **Broker集群部署** Broker集群部署时消息存储高可用的基本保障,最直接的表现是Broker出现单机故障或重启时,不会影响RocketMQ整体的服务能力。RocketMQ中Broker有四种不同的集群搭建方式: 单Master模式:单Master模式仅部署一台Broker机器,属于非集群模式,这种方式存在单点故障的风险,一旦Broker重启或者宕机,会导致整个服务不可用。不建议生产环境使用,仅可用于本地测试。 多Master模式:一个集群全部都是Master机器,没有Slave机器,属于不配置主从复制的场景,例如2个Master或者3个Master。也不建议生产环境使用,这种模式的优缺点如下: 优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可用,由于RAID10磁盘非常可靠,消息也不会丢失,性能最高。 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息的实时性会受到影响。整个缺点是致命的,消息实时性收到影响意味着一段时间内部分消费不可用,违背系统可用性原则。 异步复制的多Master多Slave模式:每个Master配置一个Slave,有多对Master-Slave,主从复制采用异步复制方式,主备有短暂的消息延迟(毫秒级),这种模式的优缺点如下: 优点:即使磁盘损坏,消息丢失非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,而且此过程对应用透明,不需要人工干预,性能同多Master模式几乎一样。 缺点:在Master宕机且磁盘损坏的情况下可能会丢失少量消息。不过出现这种场景的概率很小,有风险但是很低。 同步复制的多Master多Slave模式:每个Master配置一个Slave,有多对Master-Slave,主从复制采用同步复制方式,即只有主备都写成功,才向应用返回成功。线上推荐使用异步刷盘+同步复制的多Master多Slave模式,这种模式的优缺点如下: 优点:数据与服务都无单点故障,在Master宕机的情况下,消息无延迟,服务可用性与数据可用性都非常高。 缺点:性能比异步复制模式略低(约10%),发送单个消息的RT会略高。  # 8.解决方案 ## 8.1 分布式系统认证方案 **1.什么是认证、授权、会话?** 认证:校验用户是谁 用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信 息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手 机短信登录,指纹认证等方式 授权:解决用户能做什么 授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有 权限则拒绝访问 会话: 用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前 用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等 **2.基于session认证机制的运作流程?** 基于Session认证方式的流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话),而发 给客户端的 sesssion_id 存放到 cookie 中,这样用客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验。当用户退出系统或session过期销毁时,客户端的session_id也就无效了。 **3.基于token认证机制的运作流程?** 认证生成token,返回前端由前端维护token,请求的时候将token放在请求头中,根据token查询用户权限信息,退出生成token。 **4.理解Spring Security的工作原理,Spring Security结构总览,认证流程和授权,中间涉及到哪些组件,这些组件分** **别处理什么,如何自定义这些组件满足个性需求?** Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截, 校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。 当初始化Spring Security时,会创建一个名为SpringSecurityFilterChain 的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此 类,下图是Spring Security过虑器链结构图: ![](https://i.loli.net/2020/11/04/Q937THg1ZU6Axdb.png) FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时 这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认 证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager)进行处理,下图是FilterChainProxy相关类的UML图示。 ![](https://i.loli.net/2020/11/04/VzXk1rZBgo9xhY2.png) spring Security功能的实现主要是由一系列过滤器链相互配合完成。 下面介绍过滤器链中主要的几个过滤器及其作用: ![](https://i.loli.net/2020/11/04/OmG2biAFIZueyPq.png) SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好 的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext; UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密 码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变; FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前 面已经详细介绍过了; ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。 认证流程 ![](https://i.loli.net/2020/11/04/yulAeNWo8iO59zt.png) 认证过程: 1. 用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。 2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 3. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。 4. SecurityContextHolder 安全上下文容器将第3步填充了信息的Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。 5 OAuth2.0认证的四种模式?它们的大体流程是什么? OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不 需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版本,但不向 后兼容OAuth 1.0即完全废止了OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服 务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。 Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。 参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin Oauth协议:https://tools.ietf.org/html/rfc6 ![](https://i.loli.net/2020/11/04/sH5A632miZLnXFM.png) OAauth2.0包括以下角色: 1、客户端 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏 览器端)、微信客户端等。 2、资源拥有者 通常为用户,也可以是应用程序,即该资源的拥有者。 3、授权服务器(也称认证服务器) 用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌 (access_token),作为客户端访问资源服务器的凭据。本例为微信的认证服务器。 4、资源服务器 存储资源的服务器,本例子为微信存储的用户信息。 现在还有一个问题,服务提供商能允许随便一个客户端就接入到它的授权服务器吗?答案是否定的,服务提供商会 给准入的接入方一个身份,用于接入时的凭据: client_id:客户端标识 client_secret:客户端秘钥 因此,准确来说,授权服务器对两种OAuth2.0中的两个角色进行认证授权,分别是资源拥有者、客户端。 6 Spring cloud Security OAuth2包括哪些组件?职责? Spring-Security-OAuth2是对OAuth2的一种实现,并且跟我们之前学习的Spring Security相辅相成,与Spring Cloud体系的集成也非常便利,接下来,我们需要对它进行学习,最终使用它来实现我们设计的分布式认证授权解 决方案。 OAuth2.0的服务提供方涵盖两个服务,即授权服务 (Authorization Server,也叫认证服务) 和资源服务 (Resource Server),使用 Spring Security OAuth2 的时候你可以选择把它们在同一个应用程序中实现,也可以选择建立使用 同一个授权服务的多个资源服务。 授权服务 (Authorization Server)应包含对接入端以及登入用户的合法性进行验证并颁发token等功能,对令牌 的请求端点由 Spring MVC 控制器进行实现,下面是配置一个认证服务必须要实现的endpoints: AuthorizationEndpoint 服务于认证请求。默认 URL: /oauth/authorize 。 TokenEndpoint 服务于访问令牌的请求。默认 URL: /oauth/token 。 资源服务 (Resource Server),应包含对资源的保护功能,对非法请求进行拦截,对请求中token进行解析鉴 权等,下面的过滤器用于实现 OAuth 2.0 资源服务: OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。 ![](https://i.loli.net/2020/11/04/LOyQGYZfhDnSTR2.png) 认证流程如下: 1、客户端请求UAA授权服务进行认证。 2、认证通过后由UAA颁发令牌。 3、客户端携带令牌Token请求资源服务。 4、资源服务校验令牌的合法性,合法即返回资源信息。 7.分布式系统认证需要解决的问题? 分布式系统的每个服务都会有认证、授权的需求,如果每个服务都实现一套认证授权逻辑会非常冗余,考虑分布式 系统共享性的特点,需要由独立的认证服务处理系统认证授权的请求;考虑分布式系统开放性的特点,不仅对系统 内部服务提供认证,对第三方系统也要提供认证。分布式认证的需求总结如下: 统一认证授权 提供独立的认证服务,统一处理认证授权。 无论是不同类型的用户,还是不同种类的客户端(web端,H5、APP),均采用一致的认证、权限、会话机制,实现 统一认证授权。 要实现统一则认证方式必须可扩展,支持各种认证需求,比如:用户名密码认证、短信验证码、二维码、人脸识别 等认证方式,并可以非常灵活的切换。 应用接入认证 应提供扩展和开放能力,提供安全的系统对接机制,并可开放部分API给接入第三方使用,一方应用(内部 系统服 务)和三方应用(第三方应用)均采用统一机制接入。 技术方案 根据 选型的分析,决定采用基于token的认证方式,它的优点是: 1、适合统一认证的机制,客户端、一方应用、三方应用都遵循一致的认证机制。 2、token认证方式对第三方应用接入更适合,因为它更开放,可使用当前有流行的开放协议Oauth2.0、JWT等。 3、一般情况服务端无需存储会话信息,减轻了服务端的压力。 分布式系统认证技术方案见下图: ![](https://i.loli.net/2020/11/04/3oTM2pkrjxIzfOS.png) 流程描述: (1)用户通过接入方(应用)登录,接入方采取OAuth2.0方式在统一认证服务(UAA)中认证。 (2)认证服务(UAA)调用验证该用户的身份是否合法,并获取用户权限信息。 (3)认证服务(UAA)获取接入方权限信息,并验证接入方是否合法。 (4)若登录用户以及接入方都合法,认证服务生成jwt令牌返回给接入方,其中jwt中包含了用户权限及接入方权 限。 (5)后续,接入方携带jwt令牌对API网关内的微服务资源进行访问。 (6)API网关对令牌解析、并验证接入方的权限是否能够访问本次请求的微服务。 (7)如果接入方的权限没问题,API网关将原请求header中附加解析后的明文Token,并将请求转发至微服务。 (8)微服务收到请求,明文token中包含登录用户的身份和权限信息。因此后续微服务自己可以干两件事: 1,用户授权拦截(看当前用户是否有权访问该资源) 2,将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息) 流程所涉及到UAA服务、API网关这三个组件职责如下: 1)统一认证服务(UAA) 它承载了OAuth2.0接入方认证、登入用户的认证、授权以及生成令牌的职责,完成实际的用户认证、授权功能。、 2)API网关 作为系统的唯一入口,API网关为接入方提供定制的API集合,它可能还具有其它职责,如身份验证、监控、负载均 衡、缓存等。API网关方式的核心要点是,所有的接入方和消费端都通过统一的网关接入微服务,在网关层处理所 有的非业务功能。 ## 8.2 分布式ID #### 1.ID生成系统的需求 1.全局唯一性:不能出现重复的ID,最基本的要求。 2.趋势递增:MySQL InnoDB引擎使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应尽量使用有序的主键保证写入性能。 3.单调递增:保证下一个ID一定大于上一个ID。 4.信息安全:如果ID是连续递增的,恶意用户就可以很容易的窥见订单号的规则,从而猜出下一个订单号,如果是竞争对手,就可以直接知道我们一天的订单量。所以在某些场景下,需要ID无规则。 **第3、4两个需求是互斥的,无法同时满足。** 同时,在大型分布式网站架构中,除了需要满足ID生成自身的需求外,还需要ID生成系统可用性极高。想象以下,如果ID生成系统瘫痪,那么整个业务无法进行下去,那将是一次灾难。 因此,总结ID生成系统还需要满足如下的需求: 1.高可用,可用性达到5个9或4个9。 2.高QPS,性能不能太差,否则容易造成线程堵塞。 3.平均延迟和TP999(保证99.9%的请求都能成功的最低延迟)延迟都要尽可能低。 #### 2.分布式系统唯一ID生成策略 ##### **1. 数据库自增长序列或字段** 最常见的方式。利用数据库,全数据库唯一。 优点: 1)简单,代码方便,性能可以接受。 2)数字ID天然排序,对分页或者需要排序的结果很有帮助。 缺点: 1)不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。 2)在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。 3)在性能达不到要求的情况下,比较难于扩展。 4)如果遇见多个系统需要合并或者涉及到数据迁移会相当痛苦。 5)分表分库的时候会有麻烦。 ##### 2.UUID UUID是指在一台机器在同一时间中生成的数字在所有机器中都是唯一的。按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字 UUID由以下几部分的组合: (1)当前日期和时间。 (2)时钟序列。 (3)全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。 标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),以连字号分为五段形式的36个字符,示例:550e8400-e29b-41d4-a716-446655440000 Java标准类库中已经提供了UUID的API。 **优点** - 性能非常高:本地生成,没有网络消耗。 **缺点** - 不易存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。 - 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。 - ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用。 - 不可读。 - 没有排序,无法保证趋势递增。 ##### 3.SnowFlake雪花算法 雪花ID生成的是一个64位的二进制正整数,然后转换成10进制的数。64位二进制数由如下部分组成: ![img](https://upload-images.jianshu.io/upload_images/7432604-ed99926808fea8fe.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/967) snowflake id生成规则 - **1位标识符**:始终是0,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0。 - **41位时间戳**:41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截 )得到的值,这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的。 - **10位机器标识码**:可以部署在1024个节点,如果机器分机房(IDC)部署,这10位可以由 **5位机房ID + 5位机器ID** 组成。 - **12位序列**:毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号 **优点** - 简单高效,生成速度快。 - 时间戳在高位,自增序列在低位,整个ID是趋势递增的,按照时间有序递增。 - 灵活度高,可以根据业务需求,调整bit位的划分,满足不同的需求。 **缺点** - 依赖机器的时钟,如果服务器时钟回拨,会导致重复ID生成。 - 在分布式环境上,每个服务器的时钟不可能完全同步,有时会出现不是全局递增的情况。 ##### **4. 利用zookeeper生成唯一ID** zookeeper主要通过其znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。很少会使用zookeeper来生成唯一ID。主要是由于需要依赖zookeeper,并且是多步调用API,如果在竞争较大的情况下,需要考虑使用分布式锁。因此,性能在高并发的分布式环境下,也不甚理想。 ##### 5. MongoDB的ObjectId MongoDB的ObjectId和snowflake算法类似。它设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它 ##### 6. Redis生成ID 当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。 这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。 **1. 可以用Redis的原子操作 INCR和INCRBY来实现。** **2. 可以利用redis的lua脚本执行功能,在每个节点上通过lua脚本生成唯一ID。** 比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。 优点: 1)不依赖于数据库,灵活方便,且性能优于数据库。 2)数字ID天然排序,对分页或者需要排序的结果很有帮助。 缺点: 1)如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。 2)需要编码和配置的工作量比较大。 ## 8.3 分布式锁 ### 1.基于数据库实现分布式锁 优点:简单 缺点: 1、数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换; 2、不具备可重入的特性; 3、没有锁失效机制; 4、不具备阻塞锁特性; ### 2.基于Redis实现分布式锁 利用 Redis set key 时的一个 NX 参数可以保证在这个 key 不存在的情况下写入成功。并且再加上 EX 参数可以让该 key 在超时之后自动删除。两个命令(NX EX)一起执行。 ### 3.基于Zookeeper实现分布式锁 zookeeper的四种节点类型 1、持久化节点 :所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。 2、持久化顺序节点:这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。 3、临时节点:和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点。 4、临时顺序节点:相对于临时节点而言,临时顺序节点比临时节点多了个有序,也就是说,没创建一个节点都会加上节点对应的序号,先创建成功,序号越小。 监视器(watcher) 当zookeeper创建一个节点时,会注册一个该节点的监视器,当节点状态发生改变时,watch会被触发,zooKeeper将会向客户端发送一条通知(就一条,因为watch只能被触发一次)。 #### 1. 实现排他锁原理 简单地说就是多个客户端同时去竞争创建同一个临时子节点,Zookeeper能够保证只有一个客户端创建成功,那么这个创建成功的客户端就获得排他锁。正常情况下,这个客户端执行完业务逻辑会删除这个节点,也就是释放了锁。如果该客户端宕机了,那么这个临时节点会被自动删除,锁也会被释放。 ![img](https://img-blog.csdnimg.cn/20181115220835990.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MDExNDE=,size_16,color_FFFFFF,t_70) #### 2. 共享锁原理 ![img](https://img-blog.csdnimg.cn/20181115220943449.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MDExNDE=,size_16,color_FFFFFF,t_70) 基本原理: 创建临时有序节点,每个线程均能创建节点成功,但是其序号不同,只有序号最小的可以拥有锁,其它线程只需要监听比自己序号小的节点状态即可 基本思路如下: 1、在你指定的节点下创建一个锁目录lock(持久化节点); 2、线程X进来获取锁在lock目录下,并创建临时有序节点; 3、线程A获取lock目录下所有子节点,并获取比自己小的兄弟节点,如果不存在比自己小的节点,说明当前线程序号最小,顺利获取锁; 4、此时线程Y进来创建临时节点并获取兄弟节点 ,判断自己是否为最小序号节点,发现不是,于是设置监听(watch)比自己小的节点(这里是为了发生上面说的羊群效应); 5、线程X执行完逻辑,删除自己的节点,线程Y监听到节点有变化,进一步判断自己是已经是最小节点,顺利获取锁。 #### 3. 羊群效应 羊群是一种很散乱的组织,平时在一起也是盲目地左冲右撞,但一旦有一只头羊动起来,其他的羊也会不假思索地一哄而上,全然不顾前面可能有狼或者不远处有更好的草。因此,“羊群效应”就是比喻人都有一种从众心理,从众心理很容易导致盲从,而盲从往往会陷入骗局或遭到失败。 Zookeeper分布式锁场景中的羊群效应指的是所有的客户端都尝试对一个临时节点去加锁,当一个锁被占有的时候,其他的客户端都会监听这个临时节点。一旦锁被释放,Zookeeper反向通知添加监听的客户端,然后大量的客户端都尝试去对同一个临时节点创建锁,最后也只有一个客户端能获得锁,但是大量的请求造成了很大的网络开销,加重了网络的负载,影响Zookeeper的性能。 解决方案可以参考curator框架创建Zookeeper分布式锁的机制。原理图如下: ![img](https://img-blog.csdnimg.cn/20200316151205723.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MDExNDE=,size_16,color_FFFFFF,t_70) curator分布式锁原理 步骤为: 所有客户端都尝试去创建临时有序节点以获取锁 序号最小的临时有序节点获得锁 未获取到锁的客户端给自己的上一个临时有序节点添加监听 获得锁的客户端进行自己的操作,操作完成之后删除自己的临时有序节点 当监听到自己的上一个临时有序节点释放了锁,尝试自己去加锁 操作完成之后释放锁 之后剩下的客户端重复加锁和解锁的操作 其中最核心的思路就是获取锁时创建一个临时顺序节点,顺序最小的那个才能获取到锁,之后尝试加锁的客户端就监听自己的上一个顺序节点,当上一个顺序节点释放锁之后,自己尝试加锁,其余的客户端都对上一个临时顺序节点监听,不会一窝蜂的去尝试给同一个节点加锁导致羊群效应。 ## 8.4 分布式缓存 ### 1.数据库与缓存数据一致性解决方案 #### 1. 数据库与缓存读写模式策略的选择 ###### 1.1 为什么使用缓存 主要是从两个角度去考虑:性能和并发 使用缓存是为了提高性能,增加并发 性能 如下图所示,我们在碰到需要执行耗时特别久,且结果不频繁变动的SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。 ![图1](https://img-blog.csdnimg.cn/20200910094633984.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MDExNDE=,size_16,color_FFFFFF,t_70#pic_center) 并发 如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问数据库。 ![图2](https://img-blog.csdnimg.cn/2020091009470648.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MDExNDE=,size_16,color_FFFFFF,t_70#pic_center)不适用使用缓存的场景:数据量太大、数据访问频率非常低的业务都不适合使用Redis,数据太大会增加成本,访问频率太低,保存在内存中纯属浪费资源 带来的问题:一致性问题 数据库的数据和缓存的数据是不可能一致的,数据分为最终一致和强一致两类。 强一致:不可以使用缓存 缓存能做的只能保证数据的最终一致性。 我们能做的只能是尽可能的保证数据的一致性。 不管是先删库再删缓存 还是 先删缓存再删库,都可能出现数据不一致的情况,因为读和写操作是并发的,我们没办法保证他们的先后顺序。 具体应对策略根据业务需求来制订 首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列,分布式锁。 #### 2.解决方案 数据库与缓存读写模式策略 写完数据库后是否需要马上更新缓存还是直接删除缓存? (1)、**如果写数据库的值与更新到缓存值是一样的,不需要经过任何的计算,可以马上更新缓存,但是如果对于那种写数据频繁而读数据少的场景并不合适这种解决方案,因为也许还没有查询就被删除或修改了,这样会浪费时间和资源,对于更新和查询分开,更新不频繁,例如某些规则的配置,各服务读取,修改在后台管理系统,适合用先更新数据库在redis的方案** (2)、**如果写数据库的值与更新缓存的值不一致,写入缓存中的数据需要经过几个表的关联计算后得到的结果插入缓存中,那就没有必要马上更新缓存,只有删除缓存即可,等到查询的时候在去把计算后得到的结果插入到缓存中即可。** 所以一般的策略是当更新数据时,先删除缓存数据,然后更新数据库,而不是更新缓存,等要查询的时候才把最新的数据更新到缓存 我们常见的三种缓存更新方案: 1. 先更新数据库,再更新缓存 2. 先删除缓存,再更新数据库 3. 先更新数据库,再删除缓存 ##### 2.1 先更新数据库,再更新缓存 适合更新数据与读取缓存分离,通过后台管理系统修改一些不经常修改的数据。各服务只负责读取缓存。 **方案分析:** 这种方案有以下缺点: 1. 并发更新问题 > 比如线程A更新了数据库,线程B更新了数据库,线程B更新了缓存,线程A更新了缓存,这样最终存入的就是脏数据。 > 业务维护难度大,比如有些更新操作多,但是读取时并不多,可能浪费更新到redis的资源,另外redis缓存的数据并不一定是直接写入数据库的,可能是经过刷选,过滤,复杂计算得出的,这个时候维护麻烦,每次写入数据库,都得更新缓存,重复计算,刷选。并且不一定是更新一张表的数据要更新缓存,可能缓存跟多张表的数据有关系。 解决办法:加分布式锁,操作串行化,因为更新场景很少,数据只读,不会影响性能。 1. 数据库更新成功,缓存更新失败,数据不一致 解决办法: 1.返回前端页面失败,让前端重试,两次失败概率很小 2.通过MQ保证数据的最终一致性 ##### 2.2 先删除缓存,再更新数据库 这种方案在我们实际中使用较多,大部分都能容忍可能出现的脏数据的业务,及时出现脏数据,缓存过期后,也会读取最新的值。 **方案分析:** 存在的问题 存在脏数据的可能,比如线程A删除缓存,线程B查询缓存不存在数据,从数据库获取,获取成功后,数据存入缓存,现在A更新数据。这样缓存中的数据就是脏数据了。 解决办法:实际就是并发的问题 对于修改频繁的情况,采用双删 ```java # 删除缓存 redisConn.delete("cacheKey") # 更新数据库 db.execute("update t set count = count +1 where id = 10") # 延时删除缓存 sleep(1000) redisConn.delete("cacheKey") ``` 这种方案有以下缺点: 多次操作redis删除key 延时删除,导致接口性能不高,影响接口吞吐量 第二次可能删除失败,还是存在问题 解决方案,异步删除时可以使用MQ消息队列(比如RocketMq的延时消息),确保删除成功,删除失败则重试,这种方案对业务代码影响大,造成大量的侵入,并且MQ也可能存在消息堆积,删除延迟过长的问题。 ##### 2.3 先更新数据库,再删除缓存 先更新数据库,再删除缓存。这种方案虽然也会出现脏数据,但是概率极低,而且redis也有过期时间,能够保证最终一致性。 **方案分析:** 存在的问题 请求A查询数据库,得一个旧值,请求B将新值写入数据库,请求B删除缓存,请求A将查到的旧值写入缓存。这种情况下会存在脏数据。 出现这种问题的概率极低,除非是查询比写入慢。要解决也可以采用异步延时删除。说实话如果对于这种极低概率的脏数据都不能容忍,建议不需要使用缓存了。毕竟现在大部分都是读写分离,主从还存在延时呢。这种要强一致性的建议走mysql。对msql进行扩容比如分库分表,读写分离等等。 当然非得使用缓存又要保存数据强一致性,也有办法。采用消息队列异步删除,采用binlog同步缓存数据,删除缓存,不过这种方案代码侵入大,维护难,大部分都采用方案三。 ```java # 更新数据库 db.execute("update t set count = count +1 where id = 10") # 删除缓存 redisConn.delete("cacheKey") ``` 缓存强一致性方案流程如下: ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200910221425525.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MDExNDE=,size_16,color_FFFFFF,t_70#pic_center)另一种方案: 通过MQ串行化数据修改操作,需要评估影响 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200910221703601.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQ0MDExNDE=,size_16,color_FFFFFF,t_70#pic_center) #### 3. 总结 在评估对并发和性能影响后,通过锁避免并发问题,通过 mq,双删,设置有效期尽可能保证数据最终一致性。 ### 2.缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级 缓存雪崩: 缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。 缓存穿透: 缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。 缓存穿透解决方案: (1)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。 (2)如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓存中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴! 缓存预热: 缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! 缓存预热解决方案: (1)直接写个缓存刷新页面,上线时手工操作下; (2)数据量不大,可以在项目启动的时候自动进行加载; (3)定时刷新缓存; 缓存更新: 除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种: (1)定时去清理过期的缓存; (2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。 两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。 热点key: 热点key:某个key访问非常频繁,当key失效的时候有打量线程来构建缓存,导致负载增加,系统崩溃。 解决办法: ①使用锁,单机用synchronized,lock等,分布式用分布式锁。 ②缓存过期时间不设置,而是设置在key对应的value里。如果检测到存的时间超过过期时间则异步更新缓存。 ③在value设置一个比过期时间t0小的过期时间值t1,当t1过期的时候,延长t1并做更新缓存操作。 ## 8.5 分布式事务 1.基本的几种分布式事务解决方案?结合项目场景分析? 2.分布式事务框架? ### 1.分布式事务理论模型 BASE理论 BASE理论是由于CAP中一致性和可用性不可兼得而衍生出来的一种新的思想,BASE理论的核心思想是通过牺牲数据的强一致性来获得高可用性BASE 理论包含以下三个要素: BA:Basically Available,基本可用。 S:Soft State,软状态,状态可以有一段时间不同步。 E:Eventually Consistent,最终一致,最终数据是一致的就可以了,而不是时时保持强一致。 BASE理论并没有要求数据的强一致性,而是允许数据在一段时间内是不一致的,但是数据最终会在某个时间点实现一致。在互联网产品中,大部分都会采用BASE理论来实现数据的一致,因为产品的可用性对于用户来说更加重要。 X/Open分布式事务模型 X/Open DTP(X/Open Distributed Transaction Processing Reference Model)是X/Open这个组织定义的一套分布式事务的标准。这个标准提出了使用两阶段提交(2PC)来保证分布式事务的完整性。如下图所示,X/Open DTP中包含三种角色: AP:Application,表示应用程序。 RM:Resource Manager,表示资源管理器,比如数据库。 TM:Transaction Manager,表示事务管理器,一般指事务协调者,负责协调和管理事务,提供AP编程接口或管理RM。可以理解为Spring中提供的Transaction Manager。 致实现步骤如下: 配置TM,把多个RM注册到TM,相当于TM注册RM作为数据源。 AP从TM管理的RM中获得连接,如果RM是数据库则获取JDBC连接。 AP向TM发起一个全局事务,生成全局事务ID(XID),XID会通知各个RM。 AP通过第二步获得的连接直接操作RM完成数据操作,这时,AP在每次操作时会把XID传递给RM。 AP结束全局事务,TM会通知各个RM全局事务结束。 根据各个RM的事务执行结果,执行提交或者回滚操作。 ![img](http://cdn.processon.com/5f5dbdff7d9c085f38dee656?e=1599982608&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:LTrOyC67zIYJjUQuHiJtvg_w8K0=) 两阶段提交协议 交易中间件与数据库通过 XA 接口规范,使用两阶段提交来完成一个全局事务, XA 规范的基础是两阶段提交协议。 两阶段提交协议的执行流程如下: 准备阶段:事务管理器™通知资源管理器(RM)准备分支事务,记录事务日志,并告知事务管理器准备的结果。 提交/回滚阶段:如果所有的资源管理器(RM)在准备阶段都明确返回成功,则事务管理器(TM)向所有的资源管理器(RM)发起事务提交指令完成数据的变更。反之,如果任何一个资源管理器(RM)明确返回失败,则事务管理器(TM)会向所有资源管理器(RM)发送事务回滚指令 · 分布式事务两阶段提交,对应技术上的XA、JTA/JTS。 · XA接口 · XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。XA之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲(参考Fischer等的论文),两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。事务管理器控制着全局事务,管理事务生命周期,并协调资源。资源管理器负责控制和管理实际资源(如数据库或JMS队列) Jta规范 · 作为java平台上事务规范JTA(Java Transaction API)也定义了对XA事务的支持,实际上,JTA是基于XA架构上建模的,在JTA 中,事务管理器抽象为javax.transaction.TransactionManager接口,并通过底层事务服务(即JTS)实现。像很多其他的java规范一样,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供,目前JTA的实现主要由以下几种: · 1.J2EE容器所提供的JTA实现(JBoss) · 2.独立的JTA实现:如JOTM,Atomikos.这些实现可以应用在那些不使用J2EE应用服务器的环境里用以提供分布事事务保证。如Tomcat,Jetty以及普通的java应用。 ![img](http://cdn.processon.com/5f5dbed3f346fb47ca9ef041?e=1599982819&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:2oYPP36CcQVmPm4EuSugNimfulk=) 三阶段提交协议 · 原理 · 三阶段提交协议是两阶段提交协议的改进版本,它利用超时机制解决了同步阻塞的问题,三阶段提交协议的具体描述如下: · CanCommit(询问阶段):事务协调者向参与者发送事务执行请求,询问是否可以完成指令,参与者只需回答是或者不是即可,不需要做真正的事务操作,这个阶段会有超时中止机制。 · PreCommit(准备阶段):事务协调者会根据参与者的反馈结果决定是否继续执行,如果在询问阶段所有参与者都返回可以执行操作,则事务协调者会向所有参与者发送PreCommit请求,参与者收到请求后写redo和undo日志,执行事务操作但是不提交事务,然后返回ACK响应等待事务协调者的下一步通知。如果在询问阶段任意参与者返回不能被执行操作的结果,那么事务协调者会向所有参与者发送事务中断请求。 · DoCommit(提交或回滚阶段):这个阶段也会存在两种结果,仍然根据上一步骤的执行结果来决定DoCommit的执行方式,如果每个参与者在PreCommit阶段都返回成功,那么事务协调者会向所有参与者发起事务提交指令。反之,如果参与者中的任一参与者返回失败,那么事务协调者就会发起中止指令来回滚事务。 ![img](http://cdn.processon.com/5f5dbf6fe0b34d39660342c7?e=1599982976&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:cLXqNYoZJ7B4efeRPLzhIKzj0sg=) · 三阶段提交协议和两阶段提交协议相比有一些不同点 · 增加一个CanCommit阶段,用于询问所有参与者是否可以执行事务操作并响应,它的好处是可以尽早发现无法执行操作而中止后续的行为。 · 在准备阶段之后,事务协调者和参与者都引入了超时机制,一旦超时,事务协调者和参与者会继续提交事务,并且认为处于成功状态,因为在这种情况下事务默认为成功的可能性比较大。 · 实际上,一旦超时,在三阶段提交协议下仍然可能出现数据不一致的情况,当然概率是比较小的。另外,最大的好处就是基于超时机制来避免资源的永久锁定。 CAP定理 CAP 是指在一个分布式系统下, 包含三个要素:Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),并且 三者不可得兼得。 不同节点分布在不同的子网络中时,在内部子网络正常的情况下,由于某些原因导致这些子节点之间出现网络不通的情况,导致整个系统环境被切分成若干独立的区域,这就是网络分区。 CAP定理证明,在分布式系统中,要么满足CP、要么满足AP,不可能实现CAP或者CA。原因是网络通信并不是绝对可靠的,比如网络延时、网络异常等都会导致系统故障。而在分布式系统中,即便出现网络故障也需要保证系统仍然能够正常对外提供服务,所以在分布式系统中Partition Tolerance是必然存在的,也就是需要满足分区容错性。 如果是CA或者CAP这种情况,相当于网络百分之百可靠,否则当出现网络分区的情况时,为了保持数据的一致性,必须拒绝客户端的请求。但是如果拒绝了请求,就无法满足A,所以在分布式系统中不可能选择CA,因此只能有AP或者CP这两种选择。 AP:对于AP来说,相当于放弃了强一致性,实现最终的一致,这是很多互联网公司解决分布式数据一致性问题的主要选择。 CP:放弃了高可用性,实现强一致性。前面提到的两阶段提交和三阶段提交都采用这种方案。可能导致的问题是用户完成一个操作会等待较长的时间。 · C:Consistency,一致性,在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本) · A:Availability,可用性,在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性) · P:Partition tolerance,分区容错性,以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。 · 一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中。这就叫分区。当你一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容忍性就提高了。然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。 Eureka 保证 AP Zookeeper保证CP ### 2.柔性事务方案 #### 1.基于消息的最终一致性 基于可靠性消息的最终一致性是互联网公司比较常用的分布式数据一致性解决方案,它主要利用消息中间件(Kafka、RocketMQ或者RabbitMQ)的可靠性机制来实现数据一致性的投递。 ·异步确保型事务(会计记账)(基于可靠消息的最终一致性,可以异步,但数据绝对不能丢,而且一定要记账成功) · 示意图 ![img](http://cdn.processon.com/5dc3a9d2e4b06b7d70274477?e=1573107682&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:ADxNk5ZNmo6iTObbnVicx9FW3sE=) · 基于RocketMQ的实现 · RocketMQ为例,它提供了事务消息模型,如下图所示,具体的执行逻辑如下: ![img](http://cdn.processon.com/5f5dc20f63768955616f8f76?e=1599983648&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:_a-G0YAsNKP90gssgDkqExhJFOg=) · 生产者发送一个事务消息到消息队列上,消息队列只记录这条消息的数据,此时消费者无法消费这条消息。 · 生产者执行具体的业务逻辑,完成本地事务的操作。 · 接着生产者根据本地事务的执行结果发送一条确认消息给消息队列服务器,如果本地事务执行成功,则发送一个Commit消息,表示在第一步中发送的消息可以被消费;否则,消息队列服务器会把第一步存储的消息删除。 · 如果生产者在执行本地事务的过程中因为某些情况一直未给消息队列服务器发送确认,那么消息队列服务器会定时主动回查生产者获取本地事务的执行结果,然后根据回查结果来决定这条消息是否需要投递给消费者。 · 消息队列服务器上存储的消息被生产者确认之后,消费者就可以消费这条消息,消息消费完成之后,发送一个确认标识给消息队列服务器,表示该消息投递成功。 · 在RocketMQ事务消息模型中,事务是由生产者来完成的,消费者不需要考虑,因为消息队列可靠性投递机制的存在,如果消费者没有签收该消息,那么消息队列服务器会重复投递,从而实现生产者的本地数据和消费者的本地数据在消息队列的机制下达到最终一致。 · 不难发现,在RocketMQ的事务消息模型中最核心的机制应该是事务回查,实际上查询模式在很多类似的场景中都可以应用。在分布式系统中,由于网络通信的存在,服务之间的远程通信除成功和失败两种结果外,还存在一种未知状态,比如网络超时。服务提供者可以提供一个查询接口向外部输出操作的执行状态,服务调用方可以通过调用该接口得知之前操作的结果并进行相应处理。 #### 2.最大努力通知型 最大努力通知型和基于可靠性消息的最终一致性方案的实现是类似的,它是一种比较简单得到柔性事务解决方案,也比较适用于对数据一致性要求不高的场景,最经典的使用场景是支付宝支付结果通知,实现流程如下图所示: · 最大努力通知型事务(商户通知)按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对) · 原理图 ![img](http://cdn.processon.com/5dc3a922e4b0335f1e51e278?e=1573107507&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:R_2_ICs4LZiw1a1z6G4dvPKImhc=) ![img](http://cdn.processon.com/5f5dc2a80791296d4cbd0a2d?e=1599983800&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:V3-9eiOFUX7I1Lh1m4jNId4WHaw=) · 支付实例 #### 3.TCC补偿型 · TCC框架 ·原理图 ![img](http://cdn.processon.com/5dc3a9d2e4b06b7d70274477?e=1573107682&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:ADxNk5ZNmo6iTObbnVicx9FW3sE=) · Try: 尝试执行业务 · 完成所有业务检查(一致性) · 预留必须业务资源(准隔离性) · Confirm: 确认执行业务 · 真正执行业务 · 不作任何业务检查 · 只使用Try阶段预留的业务资源 · Confirm操作满足幂等性 · Cancel: 取消执行业务 · 释放Try阶段预留的业务资源 · Cancel操作满足幂等性 失败补偿:重试,记录错误日志 TCC事务框架会记录一些分布式事务的操作日志,保存分布式事务运行的各个阶段和状态。TCC事务协调器会根据操作日志来进行重试,以达到数据的最终一致性。 需要注意的是,TCC服务支持接口调用失败发起重试,所以TCC暴露的接口都需要满足幂等性。 基于可靠性消息的最终一致性方案 分布式事物框架LCN o [超链接 ](https://www.txlcn.org/zh-cn/index.html)分布式事务框架Seata § [超链接 ](https://www.sohu.com/a/345515118_673711)AT 模式 § AT 模式是一种无侵入的分布式事务解决方案。在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。 § AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。 · 图片 ![img](http://cdn.processon.com/5f5dc96d7d9c085f38def63a?e=1599985533&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:EZ7e12R4TJ0E4WE-TxA49RUj0Pc=) · 一阶段 · 在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。 o 图片 ![img](http://cdn.processon.com/5f5dc99ae0b34d39660350f5?e=1599985579&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:vAvLhRu4l2S9CChIitKIXClCFqs=) · 二阶段提交 · 二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可 o 图片 ![img](http://cdn.processon.com/5f5dca130791296d4cbd1503?e=1599985699&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:dIwp8GM3XgLg0PHd1BIiggDuvJk=) · 二阶段回滚 · 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。 o 图片 ![img](http://cdn.processon.com/5f5dca0de0b34d39660351c9?e=1599985693&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:nUY4jCjZUghqvObQhQUOREuFThM=) ## 8.6 分布式任务调度 o 难点 o 分布式集群的情况下,怎么保证定时任务不被重复执行 分布式锁,唯一索引 分布式任务调度平台 XXL-JOB-分布式任务调度平台 [超链接 ](#/)Elastric-Job [超链接 ](https://github.com/elasticjob)elastic-job 是由当当网基于quartz二次开发之后的分布式调度解决方案 , 由两个相对独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成 。 elastic-job主要的设计理念是无中心化的分布式定时调度框架,思路来源于Quartz的基于数据库的高可用方案。但数据库没有分布式协调功能,所以在高可用方案的基础上增加了弹性扩容和数据分片的思路,以便于更大限度的利用分布式服务器的资源。 TBSchedule [超链接 ](https://github.com/nmyphp/tbschedule)Quartz # 9.高并发与高可用 ## 1.大型网站系统应有的特点 高并发,大流量 高可用 海量数据 用户分布广泛,网络情况复杂 安全环境恶劣 渐进式发展 ## 2.网站架构演变过程 **传统架构** 传统项目分为三层架构,将业务逻辑层、数据库访问层、控制层放入在一个项目中 使用SSH或者SSM技术。 优点:适合于个人或者小团队开发,不适合大团队开发。 **SOA架构** SOA是一种软件架构模式,将共同的业务逻辑抽取出来,封装成单独的服务 业务系统分解为多个组件,让每个组件都独立提供离散,自治,可复用的服务能力 通过服务的组合和编排来实现上层的业务流程 作用:简化维护,降低整体风险,伸缩灵活 **微服务架构** 微服务是指开发一个单个、小型的但有业务的服务,每个服务都有自己的处理和轻通讯机制,可以部署在单个服务器上,让专业的人做专业的事情。微服务与SOA相比,更加轻量级。 **SOA与微服务架构区别** SOA架构主要针对企业级、采用ESB服务(ESB企业服务总线),非常重,需要序列化和反序列化,采用XML格式传输。 微服务架构主要互联网公司,轻量级、小巧,独立运行,基于Http+Rest+JSON格式传输。 ESB也可以说是传统中间件技术与XML、Web服务等技术相互结合的产物。 1.在微服务中,与SOA不同,服务可以独立于其他服务进行操作和部署,因此更容易经常部署新版本的服务和独立扩张服务,让专业的人做专业的事情,快速迭代新的产品。 2.在SOA中服务可能共享数据存储,而微服务中每个服务都具有独立的数据存储。 3.SOA与微服务主要区别在于规模和范围,SOA是一种思想,是面向服务架构体系,微服务继承了SOA的优点,去除传统的ESB消息总线,采用Http+json格式通讯方式,更加轻量级。 提出 MicroService 概念的Martin Fowler 也说过,“我们应该把 SOA 看作微服务的超集”,也就是说微服务是 SOA 的子集。 ## 3.高并发设计原则 ·系统设计不仅需要考虑实现业务功能,还要保证系统高并发、高可用、高可靠等。同时还应考虑系统容量规划(流量、容量等)、SLA指定(吞吐量、响应时间、可用性、降级方案等)、监控报警(机器负载、响应时间、可用率等)、应急预案(容灾、降级、限流、隔离、切流量、可回滚等)。 · 缓存 · 异步并发 · 连接池 · 线程池 · 扩容 · 消息队列 · 分布式任务 ### 1.拆分系统 在我们从零开始做一个新系统的时候,会首先进行系统功能模块架构设计,那么是直接做一个大而全的垂直的MVC系统,使用一个war包进行发布管理,还是需要按一些规则进行模块拆分,设计成SOA或者微服务系统比较好呢?这个笔者认为需要依据项目具有什么样的人力物力条件以及项目需要支撑多少用户量和交易量为基础。一个好的系统设计应该能够满足解决当前的需求和问题,把控实现和进度风险,预测和规划未来,避免过度设计,在上线一个基础核心版本之后,再进行不断迭代和完善。 #### **微服务系统架构设计时模块拆分的一些维度和原则** **· 系统维度** 按照系统功能、业务拆分,如、优惠券、购物车,结算,订单等系统。 **· 功能维度** 对系统功能在做细粒度拆分,优惠券系统分为 优惠券后台系统、领券系统、发券系统。 **· 读写维度** 比如商品系统中,如果查询量比较大,可以单独分为两个服务,分别为查询服务和写服务,读写比例特征拆分;读多,可考虑多级缓存;写多,可考虑分库分表. **· AOP 维度** 根据访问特征,按照 AOP 进行拆分,比如商品详情页可分为 CDN、页面渲染系统,CDN 就是一个 AOP 系统模块维度:对整体代码结构划分 Web、Service、DAO ### 2.服务化 在分布式系统中,将业务逻辑层封装成接口形式,暴露给其他系统调用,那么这个接口我们可以理解为叫做服务。 当服务越来越多的时候,就会需要用到服务治理,那么会用到Dubbo、SpringCloud服务治理框架 服务化演进: 进程内服务-单机远程服务-集群手动注册服务-自动注册和发现服务-服务的分组、隔离、路由-服务治理 考虑服务分组、隔离、限流、黑白名单、超时、重试机制、路由、故障补偿等 实践: 利用 Nginx、HaProxy、LVS 等实现负载均衡,ZooKeeper、Consul 等实现自动注册和发现服务 ### 3.消息队列 消息中间件是一个客户端与服务器异步通讯框架,消息中间件中分为点对点与发布订阅通讯方式,生产者发送消息后,消费者可以无需等待, 异步接受生产者发送消息。 在电商系统中,会使用消息队列异步推送消息,注意消息失败重试幂等性问题。 幂等性问题解决方案,使用持久化日志+全局id记录。 ### 4.缓存技术 o浏览器端缓存 o APP客户端缓存 o CDN(Content Delivery Network)缓存 o 接入层缓存 o 应用层缓存 o 分布式缓存 o 对于兜底数据或者异常数据,不应该让其缓存,否则用户会在很长一段时间里看到这些数据。 ### 5.并发化 o 改串行为并行。 ## 4.高可用设计原则 · 通过负载均衡和反向代理实现分流。 · 通过限流保护服务免受雪崩之灾。 · 通过降级实现部分可用、有损服务。 · 通过隔离实现故障隔离。 · 通过合理设置的超时与重试机制避免请求堆积造成雪崩。 · 通过回滚机制快速修复错误版本。 ### 1.降级 对于高可用服务,很重要的一个设计就是降级开关,在设计降级开关时,主要依据如下思路: 1.开关集中化管理:通过推送机制把开关推送到各个应用。 2.可降级的多级读服务:比如服务调用降级为只读本地缓存、只读分布式缓存、只读默认降级数据(如库存状态默认有货)。 3.开关前置化:如架构是Nginx–>tomcat,可以将开关前置到Nginx接入层,在Nginx层做开关,请求流量回源后端应用或者只是一小部分流量回源。 4.业务降级:当高并发流量来袭,在电商系统大促设计时保障用户能下单、能支付是核心要求,并保障数据最终一致性即可。这样就可以把一些同步调用改成异步调用,优先处理高优先级数据或特殊特征的数据,合理分配进入系统的流量,以保障系统可用。 ### 2.限流 目的: 防止恶意请求攻击或超出系统峰值 实践: 恶意请求流量只访问到 Cache 穿透后端应用的流量使用 Nginx 的 limit 处理 恶意 IP 使用 Nginx Deny 策略或iptables 拒绝 ### 3.切流量 目的:屏蔽故障机器 实践: o DNS: 更改域名解析入口,如 DNSPOD 可以添加备用 IP,正常 IP 故障时,会自主切换到备用地址; 生效实践较慢 o HttpDNS: 为了绕过运营商 LocalDNS 实现的精准流量调度 o LVS/HaProxy/Nginx: 摘除故障节点 ### 4.可回滚 发布版本失败时可随时快速回退到上一个稳定版本 ### 5.业务设计原则 1.防重设计 页面请求防止重复提交,可以采用防重key、放重表、Token等 采用图形验证,防止机器攻击。 2.幂等设计 消息中间件: 消息中间件中应该注意因网络延迟的原因,导致消息重复消费 第三方支付接口:在回调接口中,应该注意网络延迟,没有及时返回给第三方支付平台,注意回调幂等性问题。 分布式系统中,保证生成的订单号唯一性,定时Job执行的幂等性问题等。 3.后台系统操作可反馈 设计后台系统时,考虑效果的可预览、可反馈。 4.文档注释 系统发展的最初阶段就应该有文档库(设计架构、设计思想、数据字典/业务流程、现有问题),业务代码合特殊需求都要有注释。 5.备份 包括代码和人员的备份。代码主要提交到代码仓库进行管理和备份,代码仓库至少应该具备多版本的功能。人员备份指的是一个系统至少应该有两名开发人员是了解的。 ## 5.高并发服务降级特技 ### 1.服务熔断 似现实世界中的“保险丝“,当某个异常条件被触发,直接熔断整个服务,而不是一直等到此服务超时。 熔断的触发条件可以依据不同的场景有所不同,比如统计一个时间窗口内失败的调用次数。 ### 2.服务降级 有了熔断,就得有降级。所谓降级,就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。 这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景。 关于Hystrix中fallback的使用,此处不详述,参见官网。 ### 3.Hystrix(豪猪 ) 使用Hystrix实现服务隔离 Hystrix 是一个微服务关于服务保护的框架,是Netflix开源的一款针对分布式系统的延迟和容错解决框架,目的是用来隔离分布式服务故障。 它提供线程和信号量隔离,以减少不同服务之间资源竞争带来的相互影响; 提供优雅降级机制;提供熔断机制使得服务可以快速失败,而不是一直阻塞等待服务响应,并能从中快速恢复。Hystrix通过这些机制来阻止级联失败并保证系统弹性、可用。 ### 4.服务隔离 当大多数人在使用Tomcat时,多个HTTP服务会共享一个线程池,假设其中一个HTTP服务访问的数据库响应非常慢,这将造成服务响应时间延迟增加,大多数线程阻塞等待数据响应返回,导致整个Tomcat线程池都被该服务占用,甚至拖垮整个Tomcat。因此,如果我们能把不同HTTP服务隔离到不同的线程池,则某个HTTP服务的线程池满了也不会对其他服务造成灾难性故障。这就需要线程隔离或者信号量隔离来实现了。 使用线程隔离或信号隔离的目的是为不同的服务分配一定的资源,当自己的资源用完,直接返回失败而不是占用别人的资源。 Hystrix实现服务隔离两种方案 · 线程池 · 线程池方式 · 1、使用线程池隔离可以完全隔离第三方应用,请求线程可以快速放回。 · 2、请求线程可以继续接受新的请求,如果出现问题线程池隔离是独立的不会影响其他应用。 · 3、 当失败的应用再次变得可用时,线程池将清理并可立即恢复,而不需要一个长时间的恢复。 · 4、 独立的线程池提高了并发性 · 缺点: · 线程池隔离的主要缺点是它们增加计算开销(CPU)。每个命令的执行涉及到排队、调度和上 下文切换都是在一个单独的线程上运行的。 · 信号量 · 使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,当请求进来时先判断计数 器的数值,若超过设置的最大线程个数则拒绝该请求,若不超过则通行,这时候计数器+1,请求返 回成功后计数器-1。 · 与线程池隔离最大不同在于执行依赖代码的线程依然是请求线程 · tips:信号量的大小可以动态调整, 线程池大小不可以 Hystrix原理 · 熔断器开关 · 服务的健康状况 = 请求失败数 / 请求总数. · 熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的. · 当熔断器开关关闭时, 请求被允许通过熔断器. 如果当前健康状况高于设定阈值, 开关继续保持关闭. 如果当前健康状况低于 · 设定阈值, 开关则切换为打开状态. · 当熔断器开关打开时, 请求被禁止通过. · 当熔断器开关处于打开状态, 经过一段时间后, 熔断器会自动进入半开状态, 这时熔断器只允许一个请求通过. 当该请求调用 · 成功时, 熔断器恢复到关闭状态. 若该请求失败, 熔断器继续保持打开状态, 接下来的请求被禁止通过. · 熔断器的开关能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待. 并且熔断器能在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能. · 命令模式 · Hystrix使用命令模式(继承HystrixCommand类或者是HystrixObservableCommand类)来包裹具体的服务调用逻辑(run方法), 并在命令模式中添加了服务调用失败后的降级逻辑(getFallback). · 同时我们在Command的构造方法中可以定义当前服务线程池和熔断器的相关参数. · 在使用了Command模式构建了服务对象之后, 服务便拥有了熔断器和线程池的功能. ![img](http://cdn.processon.com/5e7f0d71e4b08b615740e326?e=1585388417&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:PnmQ5wyq7t6awKSMtolpRLQzcZk=) Hystrix的内部处理逻辑 · 1.构建Hystrix的Command对象, 调用执行方法. · 2.Hystrix检查当前服务的熔断器开关是否开启, 若开启, 则执行降级服务getFallback方法. · 若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否能接收新的请求, 若超过线程池已满, 则执行降级服务getFallback方法. · 若线程池接受请求, 则Hystrix开始执行服务调用具体逻辑run方法. · 3.若服务执行失败, 则执行降级服务getFallback方法, 并将执行结果上报4.Metrics更新服务健康状况. · 若服务执行超时, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况. · 若服务执行成功, 返回正常结果. · 若服务降级方法getFallback执行成功, 则返回降级结果. · 若服务降级方法getFallback执行失败, 则抛出异常. ![img](http://cdn.processon.com/5e7f0d71e4b08b615740e326?e=1585388417&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:PnmQ5wyq7t6awKSMtolpRLQzcZk=) · Hystrix Metrics的实现 · Hystrix的Metrics中保存了当前服务的健康状况, 包括服务调用总次数和服务调用失败次数等. 根据Metrics的计数, 熔断器从而 · 能计算出当前服务的调用失败率, 用来和设定的阈值比较从而决定熔断器的状态切换逻辑. 因此Metrics的实现非常重要. · 1.4之前的滑动窗口实现 · Hystrix在这些版本中的使用自己定义的滑动窗口数据结构来记录当前时间窗的各种事件(成功,失败,超时,线程池拒绝等)的计数. · 事件产生时, 数据结构根据当前时间确定使用旧桶还是创建新桶来计数, 并在桶中对计数器经行修改. · 这些修改是多线程并发执行的, 代码中有不少加锁操作,逻辑较为复杂. · ## 6.高并发服务限流特技 · 在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。 · 缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹; · 降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开; · 而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。 ### 1.为什么要互联网项目要限流 容易雪崩 o 互联网雪崩效应解决方案 o 服务降级: 在高并发的情况, 防止用户一直等待,直接返回一个友好的错误提示给客户端。 o 服务熔断:在高并发的情况,一旦达到服务最大的承受极限,直接拒绝访问,使用服务降级。 o 服务隔离: 使用服务隔离方式解决服务雪崩效应 o 服务限流: 在高并发的情况,一旦服务承受不了使用服务限流机制(计时器(滑动窗口计数)、漏桶算法、令牌桶(Restlimite)) ### 2.高并发限流解决方案 o 高并发限流解决方案限流算法(令牌桶、漏桶、计数器)、应用层解决限流(Nginx) 限流算法 #### · 计数器 · 它是限流算法中最简单最容易的一种算法,比如我们要求某一个接口,1分钟内的请求不能超过10次,我们可以在开始时设置一个计数器,每次请求,该计数器+1;如果该计数器的值大于10并且与第一次请求的时间间隔在1分钟内,那么说明请求过多,如果该请求与第一次请求的时间间隔大于1分钟,并且该计数器的值还在限流范围内,那么重置该计数器 · 滑动窗口计数 · 滑动窗口计数有很多使用场景,比如说限流防止系统雪崩。相比计数实现,滑动窗口实现会更加平滑,能自动消除毛刺。 · 滑动窗口原理是在每次有访问进来时,先判断前 N 个单位时间内的总访问量是否超过了设置的阈值,并对当前时间片上的请求数 +1。 #### · 令牌桶算法 · 令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下: · 假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌; · 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝; · 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上; · 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。 o 使用RateLimiter实现令牌桶限流 o RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率。 o 通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。 o 下面来看一些简单的实践,需要先引入guava的maven依赖。 #### · 漏桶算法 · 漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(TrafficPolicing),漏桶算法的描述如下: · 一个固定容量的漏桶,按照常量固定速率流出水滴; · 如果桶是空的,则不需流出水滴; · 可以以任意速率流入水滴到漏桶; · 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。 · 令牌桶和漏桶对比: · 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求; · 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝; · 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量; · 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率; · 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率; · 两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。 · 另外有时候我们还使用计数器来进行限流,主要用来限制总并发数,比如数据库连接池、线程池、秒杀的并发数;只要全局总请求数或者一定时间段的总请求数设定的阀值则进行限流,是简单粗暴的总数量限流,而不是平均速率限流。 · 一个固定的漏桶,以常量固定的速率流出水滴。 · 如果桶中没有水滴的话,则不会流出水滴 · 如果流入的水滴超过桶中的流量,则流入的水滴可能会发生溢出,溢出的水滴请求是无法访问的,直接调用服务降级方法,桶中的容量是不会发生变化。 #### · 漏桶算法与令牌桶算法区别 · 主要区别在于“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。 # 10.数据结构与算法 ## 1.算法时间复杂度 算法函数中n最高次幂越小,算法效率越高 总上所述,在我们比较算法随着输入规模的增长量时,可以有以下规则: 1.算法函数中的常数可以忽略; 2.算法函数中最高次幂的常数因子可以忽略; 3.算法函数中最高次幂越小,算法效率越高。 ### 1.大O记法 定义: 在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随着n的变化情况并确定T(n)的 量级。算法的时间复杂度,就是算法的时间量度,记作:T(n)=O(f(n))。它表示随着问题规模n的增大,算法执行时间 的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度,其中f(n)是问题规模n的某个函数。 在这里,我们需要明确一个事情:执行次数=执行时间 用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。一般情况下,随着输入规模n的增大,T(n)增长最 慢的算法为最优算法。 ![](https://i.loli.net/2020/12/26/yWnmA7Q9K8S5iaj.png) 他们的复杂程度从低到高依次为: O(1)0; i--) { for (int j = 0; j < i; j++) { if (greater(arr[j], arr[j + 1])) { exch(arr, j, j + 1); } } } } //比较两个元素的大小 public static boolean greater(Comparable a, Comparable b) { return a.compareTo(b) > 0; } //交换两个元素的位置 public static void exch(Comparable[] a, int i, int j) { Comparable t=a[i]; a[i]=a[j]; a[j]=t; } public static void main(String[] args) { Integer[] a = {4, 5, 6, 3, 2, 1}; Bubble.sort(a); System.out.println(Arrays.toString(a)); } } ``` **冒泡排序的时间复杂度分析** 冒泡排序使用了双层for循环,其中内层循环的循环体是真正完成排序的代码,所以, 我们分析冒泡排序的时间复杂度,主要分析一下内层循环体的执行次数即可。 在最坏情况下,也就是假如要排序的元素为{6,5,4,3,2,1}逆序,那么: 元素比较的次数为: (N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2; 元素交换的次数为: (N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2; 总执行次数为: (N^2/2-N/2)+(N^2/2-N/2)=N^2-N; 按照大O推导法则,保留函数中的最高阶项那么最终冒泡排序的时间复杂度为O(N^2). ### 2. 选择排序 选择排序是一种更加简单直观的排序方法。**需求:** 排序前:{4,6,8,7,9,2,10,1} 排序后:{1,2,4,5,7,8,9,10} #### 排序原理: 1. 每一次遍历的过程中,都假定第一个索引处的元素是最小值,和其他索引处的值依次进行比较,如果当前索引处 的值大于其他某个索引处的值,则假定其他某个索引出的值为最小值,最后可以找到最小值所在的索引 2. 交换第一个索引处和最小值所在的索引处的值 ![](https://i.loli.net/2020/12/26/5mQhkLSse6VKt8p.jpg) 选择排序API设计: | **类名** | **Selection** | | -------- | ------------------------------------------------------------ | | 构造方法 | Selection():创建Selection对象 | | 成员方法 | 1. public static void sort(Comparable[] a):对数组内的元素进行排序 2. private static boolean greater(Comparable v,Comparable w): 判 断 v 是 否 大 于 w 3.private static void exch(Comparable[] a,int i,int j):交换a数组中,索引i和索引j处的值 | #### 选择排序的代码实现: ``` public class Selection { //排序逻辑 public static void sort(Comparable[] a) { for (int i = 0; i < a.length-1; i++) { for (int j = i; j < a.length-1; j++) { if(greater(a[j],a[j+1])){ exch(a,j,j+1); } } } } //比较元素大小 public static boolean greater(Comparable a, Comparable b) { return a.compareTo(b) > 0; } //交换元素 public static void exch(Comparable[] a, int i, int j) { Comparable t = a[i]; a[i] = a[j]; a[j] = t; } public static void main(String[] args) { Integer[] a = {4, 5, 6, 3, 2, 1}; Bubble.sort(a); System.out.println(Arrays.toString(a)); } } ``` #### 选择排序的时间复杂度分析: 选择排序使用了双层for循环,其中外层循环完成了数据交换,内层循环完成了数据比较,所以我们分别统计数据 交换次数和数据比较次数: 根据大O推导法则,保留最高阶项,去除常数因子,时间复杂度为O(N^2); ### 3. 插入排序 ![img](file:///C:/Users/85889/AppData/Local/Temp/msohtmlclip1/01/clip_image003.gif) 插入排序(Insertion sort)是一种简单直观且稳定的排序算法。 插入排序的工作方式非常像人们排序一手扑克牌一样。开始时,我们的左手为空并且桌子上的牌面朝下。然后,我 们每次从桌子上拿走一张牌并将它插入左手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在 手中的每张牌进行比较,如下图所示: ![](https://i.loli.net/2020/12/26/j9tMbnGpP3vlWqi.jpg) #### 需求: 排序前:{4,3,2,10,12,1,5,6} 排序后:{1,2,3,4,5,6,10,12} #### 排序原理: 1. 把所有的元素分为两组,已经排序的和未排序的; 2. 找到未排序的组中的第一个元素,向已经排序的组中进行插入; 3. 倒叙遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素,那么就把待 插入元素放到这个位置,其他的元素向后移动一位; ![](https://i.loli.net/2020/12/26/Fa4wiCb6Wu3qoRn.jpg) 插入排序API设计: | **类名** | **Insertion** | | -------- | ------------------------------------------------------------ | | 构造方法 | Insertion():创建Insertion对象 | | 成员方法 | 1. public static void sort(Comparable[] a):对数组内的元素进行排序 2. private static boolean greater(Comparable v,Comparable w): 判 断 v 是 否 大 于 w 3.private static void exch(Comparable[] a,int i,int j):交换a数组中,索引i和索引j处的值 | **插入排序的时间复杂度分析** 插入排序使用了双层for循环,其中内层循环的循环体是真正完成排序的代码,所以,我们分析插入排序的时间复 杂度,主要分析一下内层循环体的执行次数即可。 最坏情况,也就是待排序的数组元素为{12,10,6,5,4,3,2,1},那么: 比较的次数为: (N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2; 交换的次数为: (N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2; 总执行次数为: (N^2/2-N/2)+(N^2/2-N/2)=N^2-N; 按照大O推导法则,保留函数中的最高阶项那么最终插入排序的时间复杂度为O(N^2). ### 4. 希尔排序 希尔排序是插入排序的一种,又称“缩小增量排序”,是插入排序算法的一种更高效的改进版本。 前面学习插入排序的时候,我们会发现一个很不友好的事儿,如果已排序的分组元素为{2,5,7,9,10},未排序的分组元素为{1,8},那么下一个待插入元素为1,我们需要拿着1从后往前,依次和10,9,7,5,2进行交换位置,才能完成真正的插入,每次交换只能和相邻的元素交换位置。那如果我们要提高效率,直观的想法就是一次交换,能把1放到 更前面的位置,比如一次交换就能把1插到2和5之间,这样一次交换1就向前走了5个位置,可以减少交换的次数, 这样的需求如何实现呢?接下来我们来看看希尔排序的原理。 #### 需求: 排序前:{9,1,2,5,7,4,8,6,3,5} 排序后:{1,2,3,4,5,5,6,7,8,9} ![](https://i.loli.net/2020/12/26/FqvRnMgXr5Sfcze.jpg) #### 排序原理: 1. 选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组; 2. 对分好组的每一组数据完成插入排序; 3. 减小增长量,最小减为1,重复第二步操作. 增长量h的确定:增长量h的值每一固定的规则,我们这里采用以下规则: ``` int h=1 while(h<5){ h=2h+1; //3,7 } //循环结束后我们就可以确定h的最大值; h的减小规则为: h=h/2 ``` 希尔排序的API设计: | **类名** | **Shell** | | -------- | ------------------------------------------------------------ | | 构造方法 | Shell():创建Shell对象 | | 成员方法 | 1. public static void sort(Comparable[] a):对数组内的元素进行排序 2. private static boolean greater(Comparable v,Comparable w): 判 断 v 是 否 大 于 w 3.private static void exch(Comparable[] a,int i,int j):交换a数组中,索引i和索引j处的值 | #### 希尔排序的代码实现: ``` public class Shell { //使用希尔分组选择排序 public static void sort(Comparable[] a) { int N = a.length; //确定增长量h的最大值 int h = 1; while (h < N / 2) { h = h * 2 + 1; } // 当增长量h小于1,排序结束 while (h >= 1) { // 找到待插入的元素 for (int i = h; i < N; i++) { // a[i]就是待插入的元素 // 把a[i]插入到a[i-h],a[i-2h],a[i-3h]...序列中 for (int j = i; j >= h; j -= h) { // a[j]就是待插入元素,依次和a[j-h],a[j-2h],a[j-3h]进行比较,如果a[j]小,那么 交换位置,如果不小于,a[j]大,则插入完成。 if (greater(a[j - h], a[j])) { exch(a, j, j - h); } else { break; } } } h /= 2; } } //比较两个元素的大小 public static boolean greater(Comparable a, Comparable b) { return a.compareTo(b) > 0; } //交互元素大小 public static void exch(Comparable[] a, int i, int j) { Comparable t = a[i]; a[i] = a[j]; a[j] = t; } public static void main(String[] args) { Integer[] a = {4, 5, 6, 3, 2, 1}; Bubble.sort(a); System.out.println(Arrays.toString(a)); } } ``` #### 希尔排序的时间复杂度分析 在希尔排序中,增长量h并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最 好的,对于希尔排序的时间复杂度分析,已经超出了我们课程设计的范畴,所以在这里就不做分析了。 我们可以使用事后分析法对希尔排序和插入排序做性能比较。 在资料的测试数据文件夹下有一个reverse_shell_insertion.txt文件,里面存放的是从100000到1的逆向数据,我们可以根据这个批量数据完成测试。测试的思想:在执行排序前前记录一个时间,在排序完成后记录一个时间,两个 时间的时间差就是排序的耗时。 通过测试发现,在处理大批量数据时,希尔排序的性能确实高于插入排序。 ## 4.线性表 **java中ArrayList实现?** java中ArrayList集合的底层也是一种顺序表,使用数组实现,同样提供了增删改查以及扩容等功能。 1.是否用数组实现; 2.有没有扩容操作; 3.有没有提供遍历方式; **线性表是最基本、最简单、也是最常用的一种数据结构。一个线性表是n个具有相同特性的数据元素的有限序列。** 前驱元素: 若A元素在B元素的前面,则称A为B的前驱元素后继元素: 若B元素在A元素的后面,则称B为A的后继元素 **线性表的特征**:数据元素之间具有一种“一对一”的逻辑关系。 1. 第一个数据元素没有前驱,这个数据元素被称为头结点; 2. 最后一个数据元素没有后继,这个数据元素被称为尾结点; 3. 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。 如果把线性表用数学语言来定义,则可以表示为(a1,...ai-1,ai,ai+1,...an),ai-1领先于ai,ai领先于ai+1,称ai-1是ai的前驱元素,ai+1是ai的后继元素 线性表的分类: 线性表中数据存储的方式可以是顺序存储,也可以是链式存储,按照数据的存储方式不同,可以把线性表分为顺序 表和链表。 ### 1. 顺序表 顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存 储线性表中的各个元素、使得线性表中再逻辑结构上响铃的数据元素存储在相邻的物理存储单元中,即通过数据元 素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。 **1.1.1** 顺序表的实现 顺序表API设计 | 类名 | *SequenceList | | -------- | ------------------------------------------------------------ | | 构造方法 | SequenceList(int capacity):创建容量为capacity的SequenceList对象 | | 成员方法 | 1. public void clear():空置线性表2. publicboolean isEmpty():判断线性表是否为空,是返回true,否返回false 3.public int length():获取线性表中元素的个数4. public T get(int i):读取并返回线性表中的第i个元素的值5. public void insert(int i,T t):在线性表的第i个元素之前插入一个值为t的数据元素。6.public void insert(T t):向线性表中添加一个元素t7. public T remove(int i):删除并返回线性表中第i个数据元素。8. public int indexOf(T t):返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回-1。 | | 成员变量 | 1.private T[] eles:存储元素的数组2.private int N:当前线性表的长度 | 顺序表的代码实现: ``` public class SequenceList implements Iterable { //存储元素的数组 private T[] eles; //记录当前顺序表中的元素个数 private int N; //构造方法 public SequenceList(int capacity) { eles = (T[]) new Object[capacity]; N = 0; } //容量 public int capacity() { return eles.length; } //空置线性表 public void clear() { this.N = 0; } //判断当前线性表是否为空表 public boolean isEmpty() { return this.N == 0; } //获取线性表的长度 public int length() { return this.N; } //获取指定位置的元素 public T get(int i) { if (i < 0 || i > N) { throw new RuntimeException("数组下标越界"); } return eles[i]; } //向指定位置插入元素 public void insert(int i, T t) { if (N == eles.length) { throw new RuntimeException("当前表已满"); } if (i < 0 || i > N) { throw new RuntimeException("插入的位置不合法"); } //元素已经放满了数组,需要扩容 if (N == eles.length) { resize(eles.length * 2); } // 把i位置空出来,i位置及其后面的元素依次向后移动一位 for (int j = N; j > i; j--) { eles[j] = eles[j - 1]; } eles[i] = t; N++; } //向数组末尾插入元素 public void insert(T t) { if (eles.length == N) { throw new RuntimeException("数组已满"); } eles[N++] = t; } //删除指定位置的元素并返回元素 public T remove(int i) { if (i < 0 || i > N) { throw new RuntimeException("当前元素不存在"); } T result = eles[i]; for (int j = N; j > i; j--) { eles[j] = eles[j + 1]; } N--; //当元素已经不足数组大小的1/4,则重置数组的大小 if (N > 0 && N < eles.length / 4) { resize(eles.length / 2); } return result; } //重新设置数组大小 public void resize(int newSize) { T[] oldEles = eles; eles = (T[]) new Object[newSize]; for (int i = 0; i < N; i++) { eles[i] = oldEles[i]; } } //返回元素的索引 public int indexOf(T t) { if (t == null) { throw new RuntimeException("查找的元素不合法"); } for (int i = 0; i < N; i++) { if (eles[i].equals(t)) { return i; } } return -1; } private class SIterator implements Iterator { private int cur; private SIterator() { cur = 0; } @Override public boolean hasNext() { return cur < N; } @Override public Object next() { return eles[cur++]; } } //测试代码 public static void main(String[] args) { //创建顺序表对象 SequenceList sl = new SequenceList<>(10); //测试插入 sl.insert("姚明"); sl.insert("科比"); sl.insert("麦迪"); sl.insert(1, "詹姆斯"); //测试获取 String getResult = sl.get(1); System.out.println("获取索引1处的结果为:" + getResult); //测试删除 String removeResult = sl.remove(0); System.out.println("删除的元素是:" + removeResult); //测试清空 sl.clear(); System.out.println("清空后的线性表中的元素个数为:" + sl.length()); } @Override public Iterator iterator() { return new SIterator(); } } ``` #### 顺序表的时间复杂度 get(i):不难看出,不论数据元素量N有多大,只需要一次eles[i]就可以获取到对应的元素,所以时间复杂度为O(1); insert(int i,T t):每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂为O(n); remove(int i):每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复杂度为O(n); 由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺 序表在使用过程中的时间复杂度不是线性的,在某些需要扩容的结点处,耗时会突增,尤其是元素越多,这个问题 越明显 java中ArrayList实现 java中ArrayList集合的底层也是一种顺序表,使用数组实现,同样提供了增删改查以及扩容等功能。 1.是否用数组实现; 2. 有没有扩容操作; 3. 有没有提供遍历方式; ### 2.链表 之前我们已经使用顺序存储结构实现了线性表,我们会发现虽然顺序表的查询很快,时间复杂度为O(1),但是增删的 效率是比较低的,因为每一次增删操作都伴随着大量的数据元素移动。这个问题有没有解决方案呢?有,我们可以 使用另外一种存储结构实现线性表,链式存储结构。 链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元 素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成, 结点可以在运行时动态生成。 ![](https://i.loli.net/2020/12/27/afXIBE2dDemnLkp.png) 那我们如何使用链表呢?按照面向对象的思想,我们可以设计一个类,来描述结点这个事物,用一个属性描述这个 结点存储的元素,用来另外一个属性描述这个结点的下一个结点。 结点API设计: | ***\*类名\**** | ***\*Node\**** | | -------------- | ---------------------------------------- | | 构造方法 | Node(T t,Node next):创建Node对象 | | 成员变量 | T item:存储数据Node next:指向下一个结点 | #### 1.单向链表 单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据, 指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。 ![](https://i.loli.net/2020/12/27/DGsU9n8zPyTA4N1.png) 单向链表API设计 | ***\*类名\**** | ***\*LinkList\**** | | -------------- | ------------------------------------------------------------ | | 构造方法 | LinkList():创建LinkList对象 | | 成员方法 | 1. public void clear():空置线性表2. publicboolean isEmpty():判断线性表是否为空,是返回true,否返回false 3.public int length():获取线性表中元素的个数4.public T get(int i):读取并返回线性表中的第i个元素的值5.public void insert(T t):往线性表中添加一个元素;6.public void insert(int i,T t):在线性表的第i个元素之前插入一个值为t的数据元素。7.public T remove(int i):删除并返回线性表中第i个数据元素。8.public int indexOf(T t):返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回-1。 | | 成员内部类 | private class Node:结点类 | | 成员变量 | 1.private Node head:记录首结点2.private int N:记录链表的长度 | 单向链表代码实现 ``` public class LinkList implements Iterable { class Node { //存储元素 public T item; // 指向下一个结点 public Node next; public Node(T item, Node next) { this.item = item; this.next = next; } } //记录首结点 private Node head; //记录链表的长度 private int N; public LinkList() { //初始化头结点 head = new Node(null, null); N = 0; } //获取链表长度 public int length() { return N; } //清空链表 public void clear() { head.next=null; head.item=null; N = 0; } //判断线性表是否为空,是返回true,否返回false public boolean isEmpty() { return N == 0; } //获取指定位置i出的元素 public T get(int i) { if (i < 0 || i >= N) { throw new RuntimeException("位置不合法!"); } Node n = head.next; for (int j = 0; j < i; j++) { n = n.next; } return n.item; } //往线性表中添加一个元素 public void insert(T t) { //找到最后一个元素 Node node = head.next; while (node != null) { node = node.next; } //构建一个新的节点 Node newNode = new Node(t, null); //链表长度+1 node.next = newNode; N++; } //往线性表中添加一个元素 public void insert(int i, T t) { if (i < 0 || i > N) { throw new RuntimeException("位置不合法!"); } //寻找位置i之前的结点 Node pre = head; for (int j = 0; j < i - 1; j++) { pre = pre.next; } Node cur = pre.next; Node newNode = new Node(t, cur); pre.next = newNode; N++; } //删除指定位置i处的元素,并返回被删除的元素 public T remove(int i) { if (i < 0 || i >= N) { throw new RuntimeException("位置不合法"); } // 寻找i之前的元素 Node pre = head; for (int index = 0; index <= i - 1; index++) { pre = pre.next; } // 当前i位置的结点 Node curr = pre.next; // 前一个结点指向下一个结点,删除当前结点 pre.next = curr.next; // 长度-1 N--; return curr.item; } //查找元素t在链表中第一次出现的位置 public int indexOf(T t) { Node n = head; for (int i = 0; n.next != null; i++) { n = n.next; if (n.item.equals(t)) { return i; } } return -1; } @Override public Iterator iterator() { return new LinkListIterator(); } private class LinkListIterator implements Iterator { private Node n; public LinkListIterator() { this.n = head; } @Override public boolean hasNext() { return n.next != null; } @Override public T next() { n = n.next; return n.item; } } public static void main(String[] args) throws Exception { LinkList list = new LinkList<>(); list.insert(0, "张三"); list.insert(1, "李四"); list.insert(2, "王五"); list.insert(3, "赵六"); //测试length方法 for (String s : list) { System.out.println(s); } System.out.println(list.length()); System.out.println("-------------------"); //测试get方法 System.out.println(list.get(2)); System.out.println("------------------------"); //测试remove方法 String remove = list.remove(1); System.out.println(remove); System.out.println(list.length()); System.out.println("----------------"); ; for (String s : list) { System.out.println(s); } } } ``` #### 2. 双向链表 双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用 来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点。链表的头结点的数据域不存 储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。 按照面向对象的思想,我们需要设计一个类,来描述结点这个事物。由于结点是属于链表的,所以我们把结点类作 为链表类的一个内部类来实现 结点API设计 | 类名 | Node | | -------- | ------------------------------------------------------------ | | 构造方法 | Node(T t,Node pre,Node next):创建Node对象 | | 成员变量 | T item:存储数据Node next:指向下一个结点Node pre:指向上一个结点 | 双向链表API设计 | 类名 | TowWayLinkList | | ---------- | ------------------------------------------------------------ | | 构造方法 | TowWayLinkList():创建TowWayLinkList对象 | | 成员方法 | 1. public void clear():空置线性表2. publicboolean isEmpty():判断线性表是否为空,是返回true,否返回false 3.public int length():获取线性表中元素的个数4.public T get(int i):读取并返回线性表中的第i个元素的值5.public void insert(T t):往线性表中添加一个元素;6.public void insert(int i,T t):在线性表的第i个元素之前插入一个值为t的数据元素。7.public T remove(int i):删除并返回线性表中第i个数据元素。8. public int indexOf(T t):返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回-1。9. public T getFirst():获取第一个元素10.public T getLast():获取最后一个元素 | | 成员内部类 | private class Node:结点类 | | 成员变量 | 1.private Node first:记录首结点2.private Node last:记录尾结点2.private int N:记录链表的长度 | 双向链表代码实现 ``` public class TowWayLinkList implements Iterable { @Override public Iterator iterator() { return new TIterator(); } class Node { //数据 private T item; //前节点 private Node pre; //下一个节点 private Node next; public Node(T item, Node pre, Node next) { this.item = item; this.pre = pre; this.next = next; } } //首结点 private Node head; //记录尾结点 private Node last; // 记录链表的长度 private int N; public TowWayLinkList() { last = null; head = new Node(null, null, null); N = 0; } //清空链表 public void clear() { this.head.item = null; this.last = null; this.head.next = last; N = 0; } //判断线性表是否为空,是返回true,否返回false public boolean isEmpty() { return N == 0; } //获取线性表中元素的个数 public int length() { return N; } //插入元素t public void insert(T t) { if (last == null) { last = new Node(t, head, null); head.next = last; } else { Node oldLast = last; Node node = new Node(t, oldLast, null); oldLast.next = node; last = node; } //长度+1 N++; } //向指定位置i处插入元素t public void insert(int i, T t) { if (i < 0 || i >= N) { throw new RuntimeException("位置不合法"); } //找到位置i的前一个结点 Node pre = head; for (int index = 0; index < i; index++) { pre = pre.next; } // 当前结点 Node curr = pre.next; //构建新结点 Node newNode = new Node(t, pre, curr); curr.pre = newNode; pre.next = newNode; //长度+1 N++; } //读取并返回线性表中的第i个元素的值 public T get(int i) { if (i < 0 || i >= N) { throw new RuntimeException("位置不合法"); }//寻找当前结点 Node curr = head.next; for (int index = 0; index < i; index++) { curr = curr.next; } return curr.item; } //找到元素t在链表中第一次出现的位置 public int indexOf(T t) { Node n = head; for (int i = 0; n.next != null; i++) { n = n.next; if (n.next.equals(t)) { return i; } } return -1; } //删除位置i处的元素,并返回该元素 public T remove(int i) { if (i < 0 || i >= N) { throw new RuntimeException("位置不合法"); } //寻找i位置的前一个元素 Node pre = head; for (int index = 0; index < i; index++) { pre = pre.next; } //i位置的元素 Node curr = pre.next; //i位置的下一个元素 Node curr_next = curr.next; pre.next = curr_next; curr_next.pre = pre; //长度-1; N--; return curr.item; }//获取第一个元素 public T getFirst() { if (isEmpty()) { return null; } return head.next.item; } //获取最后一个元素 public T getLast() { if (isEmpty()) { return null; } return last.item; } private class TIterator implements Iterator { private Node n = head; @Override public boolean hasNext() { return n.next != null; } @Override public Object next() { n = n.next; return n.item; } } public static void main(String[] args) throws Exception { TowWayLinkList list = new TowWayLinkList<>(); list.insert("乔峰"); list.insert("虚竹"); list.insert("段誉"); list.insert(1, "鸠摩智"); list.insert(3, "叶二娘"); for (String str : list) { System.out.println(str); } System.out.println("----------------------"); String tow = list.get(2); System.out.println(tow); System.out.println("-------------------------"); String remove = list.remove(3); System.out.println(remove); System.out.println(list.length()); System.out.println("--------------------"); System.out.println(list.getFirst()); System.out.println(list.getLast()); } } ``` java中LinkedList实现 java中LinkedList集合也是使用双向链表实现,并提供了增删改查等相关方法 1.底层是否用双向链表实现; 2.结点类是否有三个域 链表的复杂度分析 get(int i):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间复杂度为O(n) insert(int i,T t):每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n); remove(int i):每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n) 相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的, 它不需要预先指定存储空间大小,或者在存储过程中涉及到扩容等操作,,同时它并没有涉及的元素的交换。 相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删 操作比较多,建议使用链表。 ### 3. 栈 #### 1.栈概述 它是一种数据结构,数据既可以进入到栈中, 又可以从栈中出去。 栈是一种基于先进后出(FILO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出 的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一 个数据被第一个读出来)。 我们称数据进入到栈的动作为压栈,数据从栈中出去的动作为弹栈。 栈的实现 栈API设计 | 类名 | Stack | | -------- | ------------------------------------------------------------ | | 构造方法 | Stack):创建Stack对象 | | 成员方法 | 1.public boolean isEmpty():判断栈是否为空,是返回true,否返回false 2.public int size():获取栈中元素的个数3. public T pop():弹出栈顶元素4. public void push(T t):向栈中压入元素t | | 成员变量 | 1.private Node head:记录首结点2.private int N:当前栈的元素个数 | 栈代码实现 ``` public class Stack implements Iterable { private Node head; private int N; public Stack() { this.head = new Node(null, null); } //判断当前栈中元素个数是否为0 public boolean isEmpty() { return N == 0; } //把t元素压入栈 public void push(T t) { Node oldNode = head.next; Node newNode = new Node(t, oldNode); head.next = newNode; // 个数+1 N++; } //弹出栈顶元素 public T pop() { Node oldNode = head.next; if (oldNode == null) { return null; } head.next = head.next.next; N--; return oldNode.item; } //获取栈中元素的个数 public int size() { return N; } @Override public Iterator iterator() { return new SIterator(); } private class SIterator implements Iterator { private Node n = head; @Override public boolean hasNext() { return n.next != null; } @Override public T next() { Node node = n.next; n = n.next; return node.item; } } private class Node { public T item; public Node next; public Node(T item, Node next) { this.item = item; this.next = next; } } public static void main(String[] args) throws Exception { Stack stack = new Stack<>(); stack.push("a"); stack.push("b"); stack.push("c"); stack.push("d"); for (String str : stack) { System.out.print(str + " "); } System.out.println("-----------------------------"); String result = stack.pop(); System.out.println("弹出了元素:" + result); System.out.println(stack.size()); } } ``` ### 4. 队列 队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据 队列的API设计 | 类名 | Queue | | -------- | ------------------------------------------------------------ | | 构造方法 | Queue():创建Queue对象 | | 成员方法 | 1.public boolean isEmpty():判断队列是否为空,是返回true,否返回false 2.public int size():获取队列中元素的个数3.public T dequeue():从队列中拿出一个元素4.public void enqueue(T t):往队列中插入一个元素 | | 成员变量 | 1.private Node head:记录首结点2.private int N:当前栈的元素个数3.private Node last:记录最后一个结点 | 队列的实现 ``` public class Queue implements Iterable { //记录首结点 private Node head; //记录最后一个结点 private Node last; //记录队列中元素的个数 private int N; public Queue() { head = new Node(null, null); last = null; N = 0; } //判断队列是否为空 public boolean isEmpty() { return N == 0; } //返回队列中元素的个数 public int size() { return N; } public void enqueue(T t) { if (last == null) { Node lastNode = new Node(t, null); this.last = lastNode; head.next = lastNode; } else { Node oldNode = last.next; Node newNode = new Node(t, null); oldNode.next = newNode; } //个数+1 N++; } //从队列中拿出一个元素 public T dequeue() { if (isEmpty()) { return null; } Node oldFirst = head.next; head.next = oldFirst.next; N--; if (isEmpty()) { last = null; } return oldFirst.item; } @Override public Iterator iterator() { return new QIterator(); } private class QIterator implements Iterator { private Node n = head; @Override public boolean hasNext() { return n.next != null; } @Override public T next() { Node node = head.next; n = n.next; return node.item; } } private class Node { public T item; public Node next; public Node(T item, Node next) { this.item = item; this.next = next; } } public static void main(String[] args) throws Exception { Queue queue = new Queue<>(); queue.enqueue("a"); queue.enqueue("b"); queue.enqueue("c"); queue.enqueue("d"); for (String str : queue) { System.out.print(str + " "); } System.out.println("-----------------------------"); String result = queue.dequeue(); System.out.println("出列了元素:" + result); System.out.println(queue.size()); } } ``` ### 5.符号表 符号表最主要的目的就是将一个键和一个值联系起来,符号表能够将存储的数据元素是一个键和一个值共同组成的 键值对数据,我们可以根据键来查 符号表中,键具有唯一性。 符号表在实际生活中的使用场景是非常广泛的,见下表: | 应用 | 查找目的 | 键 | 值 | | -------- | ------------------------ | ------ | -------- | | 字典 | 找出单词的释义 | 单词 | 释义 | | 图书索引 | 找出某个术语相关的页码 | 术语 | 一串页码 | | 网络搜索 | 找出某个关键字对应的网页 | 关键字 | 网页名称 | 符号表API设计 结点类: | 类名 | Node | | -------- | ------------------------------------------------------------ | | 构造方法 | Node(Key key,Value value,Node next):创建Node对象 | | 成员变量 | 1.public Key key:存储键2.public Value value:存储值3.public Node next:存储下一个结点 | 符号表: | 类名 | SymbolTable | | -------- | ------------------------------------------------------------ | | 构造方法 | SymbolTable():创建SymbolTable对象 | | 成员方法 | 1. public Value get(Key key):根据键key,找对应的值2. public void put(Key key,Value val):向符号表中插入一个键值对3.public void delete(Key key):删除键为key的键值对4.public int size():获取符号表的大小 | | 成员变量 | 1.private Node head:记录首结点2.private int N:记录符号表中键值对的个数 | 符号表实现 ``` public class SymbolTable { private class Node { //键 public Key key; // 值 public Value value; // 下一个结点 public Node next; public Node(Key key, Value value, Node next) { this.key = key; this.value = value; this.next = next; } } //记录首结点 private Node head; //记录符号表中元素的个数 private int N; public SymbolTable() { head = new Node(null, null, null); N = 0; }//获取符号表中键值对的个数 public int size() { return N; } //往符号表中插入键值对 public void put(Key key, Value value) { //先从符号表中查找键为key的键值对 Node n = head; while (n.next != null) { n = n.next; if (n.key.equals(key)) { n.value = value; return; } }//符号表中没有键为key的键值对 Node oldFirst = head.next; Node newFirst = new Node(key, value, oldFirst); head.next = newFirst; //个数+1 N++; } // 删除符号表中键为key的键值对 public void delete(Key key) { Node n = head; while (n.next != null) { if (n.next.key.equals(key)) { n.next = n.next.next; N--; return; } n = n.next; } } //从符号表中获取key对应的值 public Value get(Key key) { Node n = head; while (n.next != null) { n = n.next; if (n.key.equals(key)) { return n.value; } } return null; } public static void main(String[] args) throws Exception { SymbolTable st = new SymbolTable<>(); st.put(1, "张三"); st.put(3, "李四"); st.put(5, "王五"); System.out.println(st.size()); st.put(1, "老三"); System.out.println(st.get(1)); System.out.println(st.size()); st.delete(1); System.out.println(st.size()); } } ``` ## 4.树 1.前序遍历; 先访问根结点,然后再访问左子树,最后访问右子树 2.中序遍历; 先访问左子树,中间访问根节点,最后访问右子树 3.后序遍历; 先访问左子树,再访问右子树,最后访问根节点 堆是计算机科学中一类特殊的数据结构的统称,堆通常可以被看做是一棵完全二叉树的数组对象。 ## 5.堆 **堆的特性:** 1.它是完全二叉树,除了树的最后一层结点不需要是满的,其它的每一层从左到右都是满的,如果最后一层结点不 是满的,那么要求左满右不满。 ![](https://i.loli.net/2020/12/28/Pw6fNvBtJshXjpZ.png) 2.它通常用数组来实现。 具体方法就是将二叉树的结点按照层级顺序放入数组中,根结点在位置1,它的子结点在位置2和3,而子结点的子 结点则分别在位置4,5,6和7,以此类推。 # 11.Linux 一个很全的Linux命令开源项目 https://github.com/jaywcjlove/linux-command Linux命令在线搜索网站 预览搜索:**https://git.io/linux** ## 11.1 常用命令 ### 11.1.1 Linux下文件搜索、查找、查看命令 Linux下文件搜索、查找、查看命令 *1、最强大的搜索命令:find 查找各种文件的命令  2、在文件资料中查找文件:locate   3、搜索命令所在的目录及别名信息:which  4、搜索命令所在的目录及帮助文档路径:whereis 5、在文件中搜寻字符串匹配的行并输出:grep 6、分页显示一个文件或任何输出结果:more 7、分页显示一个文件并且可以回头:less 8、指定显示前多少行文件内容:head 9、指定显示文件后多少行内容:tail 10、查看一个文件:cat 11、查看文件内容多少字符多少行多少字节:wc 12、排序文件内容:sort* > ***1、最强大的搜索命令:find 查找各种文件的命令*** **一、根据 文件或目录名称 搜索** find 【搜索目录】【-name或者-iname】【搜索字符】:-name和-iname的区别一个区分大小写,一个不区分大小写 eg:在/etc 目录下搜索名字为init的文件或目录 ①、find /etc -name init (精准搜索,名字必须为 init 才能搜索的到) ②、find /etc -iname init (精准搜索,名字必须为 init或者有字母大写也能搜索的到) ③、find /etc -name *init (模糊搜索,以 init 结尾的文件或目录名) ④、find /etc -name init??? (模糊搜索,? 表示单个字符,即搜索到 init___) **二、根据 文件大小 搜索** eg:在根目录下查找大于 100M 的文件 find / -size +204800 这里 +n 表示大于,-n 表示小于,n 表示等于 1 数据块 == 512 字节 0.5KB,也就是1KB等于2数据块 100MB == 102400KB204800数据块 **三、根据 所有者和所属组 搜索** ①、在home目录下查询所属组为 root 的文件     find /home -group root ②、在home目录下查询所有者为 root 的文件     find /home -user root **四、根据 时间属性 搜索** find 【路径】【选项】【时间】 选项有下面三种:-amin 访问时间          -cmin 文件属性被更改          -mmin 文件内容被修改 时间:+n,-n,n分别表示超过n分钟,n分钟以内和n分钟 eg:在 /etc 目录下查找5 分钟内被修改过属性的文件和目录     find /etc -cmin -5 **五、根据 文件类型或i节点 搜索**  ****\*-type 根据文件类型查找\**:***  f表示文件,d表示目录,l表示软链接 eg:查找 /home 目录下文件类型是目录的 find /home -type d   ****\*-inum 根据i节点查找\***** eg:查找 /tmp 目录下i节点为400342的文件或目录   find /tmp -inum 400342 **六、组合条件 搜索**     这里有两个参数:   ①、-a 表示两个条件同时满足(and)   ②、-o 表示两个条件满足任意一个即可(or)   范例:查找/etc目录下大于80MB同时小于100MB的文件   find /etc -size +163840 -a -size -204800 ***在文件中搜寻字符串匹配的行并输出:grep*** 功能描述:在文件中搜寻字符串匹配的行并输出  语法:grep -iv 【指定字符串】【文件】         -i 不区分大小写         -v 排除指定字符串  eg:查找 /root/install.log 文件中包含 mysql 字符串的行,并输出     grep mysql /root/install.log 本搜索工具,根据用户指定的模式,对目标文件逐行进行匹配检查,打印匹配到的行 grep是在文件中搜索匹配的字符串,是在文件中进行内容搜索,这个命令后面用到的比较多 ### 11.1.2 linux 查看ip地址、域名、端口的网络是否相通 **1.ping** ping 域名 =====》 检索当前域名对应的ip地址(实例地址) **2.telnet** telnet ip/域名 port ===> telnet 要跟端口,看端口是否想通(域名访问:http的默认端口时80,https的默认端口时443) **3、wget** wget 10.14.134.211:9080 查看地址是否能连接 **4.nslookup** nslookup 域名 ====》 检索当前域名对应的ip地址 ### 11.1.3 linux怎么看绑定的外网ip curl members.3322.org/dyndns/getip ### 11.1.4 Linux查看端口进程 方法一 1、使用命令:netstat –apn,查看所有的进程和端口使用情况,找到端口对应的PID。 \# netstat -apn 2、使用命令:ps -aux | grep pid 查看对应的进程 \# ps -aux | grep 9334 方法二 直接使用 netstat -anp | grep port \# netstat -apn | grep 8080 ### 11.1.5 文件操作 **mkdir**:新建目录 **rm**:删除文件 **rmdir**:删除空目录 **zip** 解 压:unzip FileName.zip 压缩:zip FileName.zip DirName **tar** 解 包:tar xvf FileName.tar 打包:tar cvf FileName.tar DirName **vi** test.txt //浏览文件内容 i {insert写输入} esc 退出insert :wq! write 保存并退出vi模式 :q! 不保存退出vi模式 # 12.性能优化 ## 1.tomcat性能优化 tomcat服务器在JavaEE项目中使用率非常高,所以在生产环境对tomcat的优化也变得非常重要了。 对于tomcat的优化,主要是从2个方面入手,一是,tomcat自身的配置,另一个是tomcat所运行的jvm虚拟机的调优。 详情:https://blog.csdn.net/u014401141/article/details/107523666 # 13.微服务 ## 1.微服务特点 #### 什么是微服务 微服务是系统架构上的一种设计风格,主旨是将一个原本独立的系统拆分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务之间通过基于HTTP/HTTPS协议的RESTful API进行通信协作,也可以通过RPC协议进行通信协作。被拆分成的每一个小型服务都围绕着系统中一些耦合度较高的业务功能进行构建,并且每个服务都维护着自身的数据存储,业务开发,自动化测试案例以及独立部署机制。由于有了轻量级的通信协作基础,所以这些微服务可以使用不同的语言来编写。 #### 微服务特点 相比较于单体应用架构和SOA架构,微服务架构的主要特点是组件化、松耦合、自治、去中心化,体现在以下几个方面:用 4个字描述就是小 独 轻 松 小:体现每个微服务粒度要小,而每个服务是针对一个单一职责的业务能力的封装,专注做好一件事情。 独:独立部署运行和扩展。每个服务能够独立被部署并运行在一个进程内。这种运行和部署方式能够赋予系统灵活的代码组织方式和发布节奏,使得快速交付和应对变化成为可能。 轻:系统相比较复杂单体应用更为简洁轻量化,每个微服务因为独立部署,可以使用不同跨语言编写,这样使得微服务架构更为灵活. 松:低耦合性,符合面向对象设计高内聚低耦合特性。不同模块间依赖低,相互关联小(因为每个微服务设计的初衷是每个服务专注一个模块开发) ## 2.分布式服务接口的幂等性如何设计 ## 3.CAP定理 - 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本) - 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据) - 分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。) ## 4.分布式锁 14.面试的一些技巧 需要准备优化的 自我介绍 准备⾃⼰的⾃我介绍 ⾃我介绍⼀般是你和⾯试官的第⼀次⾯对⾯正式交流,换位思考⼀下,假如你是⾯试官的话,你想听到 被你⾯试的⼈如何介绍⾃⼰呢?⼀定不是客套地说说⾃⼰喜欢编程、平时花了很多时间来学习、⾃⼰的 兴趣爱好是打球吧? 我觉得⼀个好的⾃我介绍应该包含这⼏点要素: \1. ⽤简单的话说清楚⾃⼰主要的技术栈于擅⻓的领域; \2. 把重点放在⾃⼰在⾏的地⽅以及⾃⼰的优势之处; \3. 重点突出⾃⼰的能⼒⽐如⾃⼰的定位的bug的能⼒特别厉害; 项目介绍 准备好⾃⼰的项⽬介绍 如果有项⽬的话,技术⾯试第⼀步,⾯试官⼀般都是让你⾃⼰介绍⼀下你的项⽬。你可以从下⾯⼏个⽅ 向来考虑: \1. 对项⽬整体设计的⼀个感受(⾯试官可能会让你画系统的架构图) \2. 在这个项⽬中你负责了什么、做了什么、担任了什么⻆⾊ \3. 从这个项⽬中你学会了那些东⻄,使⽤到了那些技术,学会了那些新技术的使⽤ \4. 另外项⽬描述中,最好可以体现⾃⼰的综合素质,⽐如你是如何协调项⽬组成员协同开发的或者 在遇到某⼀个棘⼿的问题的时候你是如何解决的⼜或者说你在这个项⽬⽤了什么技术实现了什么 功能⽐如:⽤redis做缓存提⾼访问速度和并发量、使⽤消息队列削峰和降流等等 你不会的东⻄就不要写在简历上。另外,你要考虑你该如何才能让你 的亮点在简历中凸显出来,⽐如:你在某某项⽬做了什么事情解决了什么问题(只要有项⽬就⼀定有要 解决的问题)、你的某⼀个项⽬⾥使⽤了什么技术后整体性能和并发量提升了很多等等。 ⾯试和⼯作是两回事,聪明的⼈会把⾯试官往⾃⼰擅⻓的领域领,其他⼈则被⾯试官牵着⿐⼦⾛。虽说 ⾯试和⼯作是两回事,但是你要想要获得⾃⼰满意的 offer ,你⾃身的实⼒必须要强。 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你 会很有礼貌。 准备好⾃⼰的项⽬介绍。 如果有项⽬的话,技术⾯试第⼀步,⾯试官⼀般都是让你⾃⼰介绍⼀ 下你的项⽬。你可以从下⾯⼏个⽅向来考虑:①对项⽬整体设计的⼀个感受(⾯试官可能会让你 画系统的架构图;②在这个项⽬中你负责了什么、做了什么、担任了什么⻆⾊;③ 从这个项⽬ 中你学会了那些东⻄,使⽤到了那些技术,学会了那些新技术的使⽤;④项⽬描述中,最好可以 体现⾃⼰的综合素质,⽐如你是如何协调项⽬组成员协同开发的或者在遇到某⼀个棘⼿的问题的 时候你是如何解决的⼜或者说你在这个项⽬⽤了什么技术实现了什么功能⽐如:⽤redis做缓存提 ⾼访问速度和并发量、使⽤消息队列削峰和降流等等。 \7. 提前知道有哪些技术问题常问**:** 索引、隔离界别、HashMap源码分析、SpringMVC执⾏过程等等问 题我觉得⾯试中实在太常⻅了,好好准备!后⾯的⽂章会我会分类详细介绍到那些问题最常问。 \8. 提前熟悉⼀些常问的⾮技术问题**:** ⾯试的时候有⼀些常⻅的⾮技术问题⽐如“⾯试官问你的优点 是什么,应该如何回答?”、“⾯试官问你的缺点是什么,应该如何回答?”、“如果⾯试官问"你有 什么问题问我吗?"时,你该如何回答”等等,对于这些问题,如何回答⾃⼰⼼⾥要有个数,别⾯试 的时候出了乱⼦。 \9. ⾯试之后记得复盘。 ⾯试遭遇失败是很正常的事情,所以善于总结⾃⼰的失败原因才是最重要 的。如果失败,不要灰⼼;如果通过,切勿狂喜。