JDK1.7及其之前:数组,链表 ; JDK1.8开始:数组,链表,红黑树
关键字:相同Hash。解决方案:拉链法,再哈希法,换算法,溢出表
哈希冲突,也叫哈希碰撞,指的是两个不同的值,计算出了相同的hash,也就是两个不同的数据计算出同一个下标,通常解决方案有:
关键字:hash冲突,不同key相同hash,挂载链表
当我们向HashMap中添加元素时,会先根据key进行哈希运算,把hash值模与数组长度得到一个下标,然后将该元素添加进去。但是如果产生了哈希碰撞,也就是不同的key计算出了相同的hash值,这就出问题了,因此它采用了拉链法来解决这个问题,将产生hash碰撞的元素,挂载到链表中
关键字:链表越长,查询越慢。超过8转为红黑树。
当HashMap中同一个索引位置出现哈希碰撞的元素多了,链表会变得越来越长,查询效率会变得越来越慢。因此在JDK1.8之后,当链表长度超过8个,会将链表转换为红黑树来提高查询
关键字:链表8,数组64.节点数6
当链表的长度大于等于8,同时数组的长度大于64,链表会自动转化为红黑树,当树中的节点数小于等于6,红黑树会自动转化为链表
关键字:12个扩容。成倍扩容。
HashMap的数组初始容量是16,负载因子是0.75,也就是说当数组中的元素个数大于12个,会成倍扩容
tips:为啥子是0.75?:关键字:过小,过大,折中。负载因子过小容易浪费空间,过大容易造成更多的哈希碰撞,产生更多的链表和树,因此折衷考虑采用了0.75
为啥子是成倍扩容?:需要保证数组的长度是2的整数次幂
为嘛数组的长度必须是2的整数次幂:
关键字:(n-1)*hash,减少hash碰撞。
hashMap成倍扩容
我们在存储元素到数组中的时候,是通过hash值模与数组的长度,计算出下标的。但是由于计算机的运算效率,加减法>乘法>除法>取模,取模的效率是最低的。开发者们为了让你用的开心,也是呕心沥血。将取模运算转化成了与运算,即数组长度减1的值和hash值的与运算,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞以此来优化性能。但是这个转化有一个前提,就是数组的长度必须为2的整数次幂
关键字:hash运算,没有就放在此位置,有则比较相同-覆盖数据,不同-判断红黑树/链表
首先,将key进行hash运算,将这个hash值与上当前数组长度减1的值,计算出索引。此时判断该索引位置是否已经有元素了,如果没有,就直接放到这个位置
如果这个位置已经有元素了,也就是产生了哈希碰撞,那么判断旧元素的key和新元素的key的hash值是否相同,并且将他们进行equals比较,如果相同证明是同一个key,就覆盖旧数据,并将旧数据返回,如果不相同的话
再判断当前桶是链表还是红黑树,如果是红黑树,就按红黑树的方式,写入该数据,
如果是链表,就依次遍历并比较当前节点的key和新元素的key是否相同,如果相同就覆盖,如果不同就接着往下找,直到找到空节点并把数据封装成新节点挂到链表尾部。然后需要判断,当前链表的长度是否大于转化红黑树的阈值,如果大于就转化红黑树,最后判断数组长度是否需要扩容。
关键字:计算索引位置,key不匹配红黑树/链表;
首先将key进行哈希运算,计算出数组中的索引位置,判断该索引位置是否有元素,如果没有,就返回null,如果有值,判断该数据的key是否为查询的key,如果是就返回当前值的value
如果第一个元素的key不匹配,判断是红黑树还是链表,如果是红黑树,就就按照红黑树的查询方式查找元素并返回,如果是链表,就遍历并匹配key,然后返回value值
关键字:多线程出现死循环,currentHashMap线程安全
HashMap在扩容数组的时候,会将旧数据迁徙到新数组中,这个操作会将原来链表中的数据颠倒,比如a->b->null,转换成b->a->null
这个过程单线程是没有问题的,但是在多线程环境,就可能会出现a->b->a->b…,这就是死循环
在JDK1.8后,做了改进保证了转换后链表顺序一致,死循环问题得到了解决。但还是会出现高并发时数据丢失的问题,因此在多线程情况下还是建议使用ConcurrentHashMap来保证线程安全问题
关键字:线程安全。分段锁,互斥锁。CAS、synchronized实现
ConcurrentHashMap,它是HashMap的线程安全,支持高并发的版本
在jdk1.7中,它是通过分段锁的方式来实现线程安全的。意思是将哈希表分成许多片段Segment,而Segment本质是一个可重入的互斥锁,所以叫做分段锁。
在jdk1.8中,它是采用了CAS操作和synchronized来实现的,而且每个Node节点的value和next都用了volatile关键字修饰,保证了可见性
put值 put(“laoyang”,“你好啊”);
1.使用hash函数对key。计算hash值 hash(“laoyang”)=35
2.在根据散列值模数组长度 35/16=33.找到3对应的数组下标位置,判断
3对应的数组下标位置,是否已经存在元素了
3.1 如果不存在,直接放入数组位置即可 Node
3.1.1 如果放入元素后,发现数字的内容已经装了超过0.75负载因子(12个),会执行数组自动扩容–翻倍
3.2 如果已经存在值,去看可以是否一致
3.2.1 如果key一致,直接覆盖value
3.2.2 如果key不一致,使用拉链表的方是来存储Node数据
4.如果链表的长度超过8,自动将链表转换为红黑树
5.如果红黑树的节点树小于
6,自动将红黑树转换为链表
get一个值get(“laoyang”)
1.使用hash函数对key。计算hash值 hash(“laoyang”)=35
2.在根据散列值模数组长度 35/16=3
3.找到3对应的数组下标位置,判断3对应的数组下标位置,是否只有一个node
3.1 只有一个,直接返回value给你
3.2 不止一个,需要判断类型是链表还是红黑树
3.2.1 链表拿值 循环遍历链表,拿到每一个节点Node,比较key和需要搜索的key是否一致,如果一致,说明找到了,将value返回 如果key不一致,找下一个,直到遍历完,如果都没有,那就返回null
关键字:spring启动:加载配置文件,
当Spring启动时,IOC容器会加载Spring的配置文件,包括XML配置或者注解,然后解析这些Bean并把相关定义信息封装成BeanDefinition对象,通过Bean注册器BeanDefinitionRegistry注册到IOC容器,也就是一个ConcurrentHashMap中
此时会找出所有的单例且非惰性加载的bean,根据其BeanDefinition进行Bean的实例化,它会判断如果bean中有方法覆盖,就使用JDK反射创建Bean,否则使用CGLIB方式生成代理。然后把实例化好的Bean缓存到一个ConcurrentHashMap中
从宏观的角度来说就是:实例化 ,属性注入,初始化,使用,销毁。更细的生命周期如下
关键字:IOC
BeanFactory接口是IOC容器的核心接口,定义了管理bean的最基本方法,比如实例化,配置,管理,获取bean的方法
FactoryBean是IOC容器创建bean的一种形式,可以通过实现此接口来创建实例化过程比较复杂的bean
关键字:放入ConcrurrentHashMap中,线程安全,直接在map中获取。
IOC容器会将单例模式的bean放入一个ConcurrentHashMap中,需要这个bean时直接到这个map中获取,如果没有找到才会实例化这个bean。而ConcurrentHashMap本身是线程安全的,也就保证了Bean是单例的。
循环依赖分为三种,构造器注入循环依赖 ,setter方式注入循环依赖,多例模式Bean的循环依赖。而Spring解决了单例bean的setter注入循环依赖
setter循环依赖的解决主要使用了三级缓存
假设有两个bean,A依赖B,B依赖A
1、A 实例化时依赖 B,于是 A 先放入三级缓存,然后去实例化 B;
2、B 进行实例化,把自己放到三级缓存中,然后发现又依赖于 A,于是先去查找缓存,但一级二级都没有,在三级缓存中找到了。
···然后把三级缓存里面的 A 放到二级缓存,然后删除三级缓存中的 A;
···然后 B 注入半成品的实例 A 完成实例化,并放到一级缓存中;
3、然后回到 A,因为 B 已经实例化完成并存在于一级缓存中,所以直接从一级缓存中拿取,然后注入B,完成实例化,再将自己添加到一级缓存中。
源码刨析
————————————————
当实例化好A,在属性注入时发现A依赖B,会先将正在创建的A的实例工厂ObjectFactory放入三级缓存,然后去创建B的实例。
走Bean的实例化流程创建B,在B的属注入环节发现,B依赖了A,这个时候就会去三级缓存中,找到A的创建工厂ObjectFactory获取A的实例,并注入到B中。此时B就初始化好了,然后将B实例放入一级缓存。最后将B实例注入到A中,A也就创建好了
在getBean的时候,如果单例Bean缓存池没有Bean,就会走二级缓存尝试获取,如果也没有,就会走三级缓存拿到Bean的ObjectFacory创建Bean,然后把Bean放入二级缓存。
构造注入不能解决循环依赖的原因是:如果A的构造其中依赖了B,B的构造器中又依赖了A ,在getSingleton中三级缓存需要调用getObject()构造器,来构造提早暴露但未设置属性的bean,此时就会产生无限递归创建
多例模式下Bean是不做缓存的,所以就没法暴露ObjectFactory,也就没办法解决循环依赖
BeanFactory:IOC容器顶层接口,提供了Bean获取的基础方法
DefaultListableBeanFactory:是整个 bean 加载的核心部分,Spring 注册及加载Bean 的默认实现
ApplicationContext:除了实现IOC基本功能外,还扩展了国际化支持,资源访问,事件发布
ClasspathXmlApplicationContext:从classpath中获取XML配置
gjz:动态代理,生成代理类以达到代码增强的目的。JDK,cglib。
AOP的实现原理是基于动态代理,动态代理就是在运行时期动态的为原生类生成代理类以达到代码增强的目的,且代理类是持有原生类的,可以在代理类中调用原生类以及做一些增强业务。
动态代理分为JDK动态代理和CGLIB代理,CGLIB代理需要导入相关的jar包,两者的区别是JDK动态代理要求目标类需要实现至少一个接口。而CGLIB((Code Generation Library))则是基于继承进行代理,原生类可以不实现任何接口
Spring中默认采用JDK动态代理,如果原生类没有实现任何接口,Spring会选择CGLIB代理,或者你可以在配置文件中强制指定使用CGLIB代理
gjz:通过后置处理器完成。在Bean实例化中触发了后置处理器中的一个方法,扫描当前类是否有@Autowired,得到自动注入的依赖的bean的类型,去容器得到依赖的bean的实例。
自动注入是通过BeanPostProcessor 后置处理器(AutowiredAnnotationBeanPostProcessor)完成的。
在Bean实例化过程中,触发了AutowiredAnnotationBeanPostProcessor的postProcessPropertyValues方法的调用执行,它就会扫描当前类中是否有@Autowired注解,然后得到自动注入依赖的bean的类型,并去容器中得到依赖的bean实例,如果没有 就走Bean的实例化流程创建依赖的Bean,然后反射进行字段赋值。
分为两个动作,第一个是解析@Transcational注解,在Sping中有个后置处理器InfrastructureAdvisorAutoProxyCreator,在Bean的初始化过程中,它负责解析标记了@Transcational注解的类,生成代理。还创建了 TransactionAttributeSource ,它是对事务注解的封装,以及 TransactionInterceptor 事务拦截器。
在执行业务方法的时候,代码会进入事务拦截器TransactionInterceptor去执行事务相关的代码,TransactionInterceptor主要是通过调用TranscationManagerment的事务API,而TranscationManagerment又是调用connection的事务API完成事务操作。
关键字:贴,切这个注解,相关参数,
可以自定义一个注解贴在方法上。使用Spring的AOP去切这个注解,然后拿到方法的相关参数,类名,方法名,时间等信息可以往数据库保存,要求高性能的话可以开线程异步保存到ES中。
限流就是现在某个方法的最大请求数量,可以自定义一个注解贴在方法上。使用Spring的AOP去切这个注解,然后定一个限流数量比如:100,再定义一个计数器默认0,当请求执行进来方法就计数器+1,当请求执行完就计数器-1,如果当计数器达到100就把请求拒绝掉就可以了。
在启动类上我们会打上: @SpringBootApplication 注解,它是一个组合标签,包括:
那它是怎么去找到这些所谓的自动配置类的呢?
gjz:扫描jar包自动配置类。
他会通过Spring的SPI接口,也就是通过一个SpringFactoryLoader去扫描 classpath中的所有的jar包中的 MET-INF/spring.factories 中的自动配置类,比如: DispatchServlert就对应了DispatchServlertAutoConfiguration自动配置类 , 它通过@Bean+方法的方式注册了一个 DispatchServlert 到Spring容器中了
1.开启秒表计时
2.starting监听器,
3.处理应用参数
4.加载环境对象
5.打印横幅
6.创建Spring容器对象:AnnotationConfigApplicationContext
7.容器刷新的前置工作
8.刷新容器 ,这里会执行spring的ioc属性容器的refrsh方法,Bean的加载,初始化等都在这个里面,Tomcat的启动也在这个方法里面。
9.刷新容器后置工作
10.秒表停止
11.started事件
12.调用runner
13.running.listeners.
springBoot如何定义Starter
springBoot系列
在starter中不仅包含了集成某个组件所需要的所有jar, 还包括集成该组件的配置类。总结如下:
它整合了这个模块需要的依赖库;
提供对模块的配置项给使用者;
提供自动配置类对模块内的Bean进行自动装配;
吃透Mybatis源码
执行流程
1、DispatcherServlet:请求打过来由DispatcherServlet处理,它是 SpringMVC 中的前端控制器(中央控制器), 负责接收 Request 并将 Request 转发给对应的处理组件。
2、HandlerMapping:HandlerMapping 维护了 url 和 Controller(Handler)的 映 射关系 。 DispatcherServlet 接 收 请求, 然 后 从 HandlerMapping 查找处理请求的Controller(Handler),标注了@RequestMapping 的每个 method 都可以看成是一个 Handler,HandlerMapping 在请求到达之后, 它的作用便是找到请求相应的处理器 Handler 和 Interceptors。
3、HandlerAdapter:SpringMVC通过HandlerAdapter对Handler进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。它的作用就是按照特定的规则去执行 Controller (Handler)
4、Handler : Controller (Handler)负责处理请求,Controller 执行后并返回 ModelAndView 对象,其中包括了数据模型和逻辑视图,ModelAndView 是封装结果 视图的组件。Handler把结果返回给HandlerAdapter,HandlerAdapter把结果返回给DispatcherServlet前端控制器。
5、ViewResolver:DispatcherServlet收到ModelAndView,调用视图解析器(ViewResolver)来解析HandlerAdapter传递的ModelAndView。Handler执行完成后返回的是逻辑视图,也就是视图名字,一个String ,还有一个Model就是数据模型,封装成ModelAndView。ViewResolver视图解析的作用就是根据视图名,把本地模板文件(比如:xx.jsp;xx.ftl)解析为View视图对象。View用来渲染视图,也就是负责把Handler返回的数据模型model,填充到模板(jsp;ftl)形成html格式的静态内容。
6、最后就是把生成的html通过response写给浏览器,浏览器进行html渲染展示。
SpringMVC执行流程
SpringMVC
Redis底层原理
方式一:继承Thread类,覆写run方法,创建实例对象,调用该对象的start方法启动线程
方式二:创建Runnable接口的实现类,类中覆写run方法,再将实例作为此参数传递给Thread类有参构造创建线程对象,调用start方法启动
方式三:创建Callable接口的实现类,类中覆写call方法,创建实例对象,将其作为参数传递给FutureTask类有参构造创建FutureTask对象,再将FutureTask对象传递给Thread类的有参构造创建线程对象,调用start方法启动
Thread有单继承的局限性,Runnable和Callable避免了单继承的局限,使用更广泛。Runnable适用于无需返回值的场景,Callable使用于有返回值的场景
start是开启新线程, 而调用run方法是一个普通方法调用,还是在主线程里执行。没人会直接调用run方法
第一,sleep方法是Thread类的静态方法,wait方法是Object类的方法
第二:sleep方法不会释放对象锁,wait方法会释放对象锁
第三:sleep方法必须捕获异常,wait方法不需要捕获异常
第四:wait需要被唤醒notify,
新建状态:线程刚创建,还没有调用start方法之前
就绪状态:也叫临时阻塞状态,当调用了start方法后,具备cpu的执行资格,等待cpu调度器轮询的状态
运行状态:就绪状态的线程,获得了cpu的时间片,真正运行的状态
冻结状态:也叫阻塞状态,指的是该线程因某种原因放弃了cpu的执行资格,暂时停止运行的状态,比如调用了wait,sleep方法
他们都是用来解决并发编程中的线程安全问题的,不同的是
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制,它维护了一个volatile修饰的 int 类型的,state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
工作思想是如果被请求的资源空闲,也就是还没有线程获取锁,将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果请求的资源被占用,就将获取不到锁的线程加入队列。
悲观锁和乐观锁,指的是看待并发同步问题的角度
悲观锁认为,对同一个数据的并发操作,一定是会被其他线程同时修改的。所以在每次操作数据的时候,都会上锁,这样别人就拿不到这个数据。如果不加锁,并发操作一定会出问题。用阳间的话说,就是总有刁民想害朕
乐观锁认为,对同一个数据的并发操作,是不会有其他线程同时修改的。它不会使用加锁的形式来操作数据,而是在提交更新数据的时候,判断一下在操作期间有没有其他线程修改了这个数据
悲观锁一般用于并发小,对数据安全要求高的场景,乐观锁一般用于高并发,多读少写的场景,通常使用版本号控制,或者时间戳来解决.
CAS,compare and swap的缩写,中文翻译成比较并交换。它是乐观锁的一种体现,CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
实例方法上的锁,锁住的是这个对象实例,它不会被实例共享,也叫做对象锁
静态方法上的锁,锁住的是这个类的字节码对象,它会被所有实例共享,也叫做类锁
Synchronized(this) 中,this代表的是该对象实例,不会被所有实例共享
Synchronized (User.class),代表的是对类加锁,会被所有实例共享
这两个关键字都是用来解决并发编程中的线程安全问题的,不同点主要有以下几点
第一:volatile的实现原理,是在每次使用变量时都必须从主存中加载,修改变量后都必须立马同步到主存;synchronized的实现原理,则是锁定当前变量,让其他线程处于阻塞状态
第二:volatile只能修饰变量,synchronized用在修饰方法和同步代码块中
第三:volatile修饰的变量,不会被编译器进行指令重排序,synchronized不会限制指令重排序
第四:volatile不会造成线程阻塞,高并发时性能更高,synchronized会造成线程阻塞,高并发效率低
第五:volatile不能保证操作的原子性,因此它不能保证线程的安全,synchronized能保证操作的原子性,保证线程的安全
synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低,涉及到用户态到内核态的切换,会让整个程序性能变得很差。
因此在JDK1.6及以后的版本中,增加了锁升级的过程,依次为无锁,偏向锁,轻量级锁,重量级锁。而且还增加了锁粗化,锁消除等策略,这就节省了锁操作的开销,提高了性能(没有线程抢占资源无锁,轻量级锁自旋10次变成重量级锁)
每个对象都拥有对象头,对象头由Mark World ,指向类的指针,以及数组长度三部分组成,锁升级主要依赖Mark Word中的锁标志位和释放偏向锁标识位。
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程 获得锁之后(线程的id会记录在对象的Mark Word锁标志位中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。(第二次还是这个线程进来就不需要重复加锁,基本无开销),如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁自旋锁);没有抢到锁的线程将自旋,获取锁的操作。轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord锁标志位更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)
如果锁竞争情况严重,某个达到最大自旋次数(10次默认)的线程,会将轻量级锁升级为重量级锁,重量级锁则直接将自己挂起,在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。
场景一:ES中对version的控制并发写。
场景二:数据库中使用version版本号控制来防止更新覆盖问题。
场景三:原子类中的CompareAndSwap操作
通过CAS操作原理来实现的,就可见性和原子性两个方面来说
它的value值使用了volatile关键字修饰,也就保证了多线程操作时内存的可见性
Unsafe这个类是一个很神奇的类,而compareAndSwapInt这个方法可以直接操作内存,依靠的是C++来实现的,它调用的是Atomic类的cmpxchg函数。而这个函数的实现是跟操作系统有关的,比如在X86的实现就利用汇编语言的CPU指令lock cmpxchg,它在执行后面的指令时,会锁定一个北桥信号,最终来保证操作的原子性
public class AtomicDemo implements Runnable {
private AtomicInteger number = new AtomicInteger(100);
private String name = "";
AtomicDemo(String name){
this.name = name;
}
@Override
public void run() {
System.out.println("线程:"+this.name+" 执行...");
for (int i = 0 ; i < 100 ; i++) {
if (number.get() > 0) {
try {
Thread.sleep(100);
//获取和递减值,内部基于乐观锁保证原子性
System.out.println(Thread.currentThread().getName() + ":卖出:" +number.getAndDecrement());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
AtomicDemo runnableDemo = new AtomicDemo("RunnableDemo");
new Thread(runnableDemo).start();
new Thread(runnableDemo).start();
}
}
可重入锁是指允许同一个线程多次获取同一把锁,比如一个递归函数里有加锁操作
自旋锁不是锁,而是一种状态,当一个线程尝试获取一把锁的时候,如果这个锁已经被占用了,该线程就处于等待状态,并间隔一段时间后再次尝试获取的状态,就叫自旋(适应性自选,干点其它事情再自旋,或者达到一定次数就不自旋)
阻塞,指的是当一个线程尝试获取锁失败了,线程就就进行阻塞,这是需要操作系统切换CPU状态的
Lock锁体系 ,ConcurrentHashMap ,Atomic原子类,如:AtomicInteger ;ThreadLoal ; ExecutorService
线程独占
ThreadLocal,翻译成中国话,叫做线程本地变量,它是为了解决线程安全问题的,它通过为每个线程提供一个独立的变量副本,来解决并发访问冲突问题 - 简单理解它可以把一个变量绑定到当前线程中,达到线程间数据隔离目的。
原理:ThredLocal是和当前线程有关系的,每个线程内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,它用来存储每个线程中的变量副本,key就是ThreadLocal变量,value就是变量副本。
当我们调用get方法是,就会在当前线程里的threadLocals中查找,它会以当前ThreadLocal变量为key获取当前线程的变量副本
它的使用场景比如在spring security中,我们使用SecurityContextHolder来获取SecurityContext,比如在springMVC中,我们通过RequestContextHolder来获取当前请求,比如在 zuul中,我们通过ContextHolder来获取当前请求
请求并发高的时候,如果没有线程池会出现线程频繁创建和销毁而浪费性能的情况,同时没办法控制请求数量,所以使用了线程池后有如下好处
CachedThreadPool:可缓存的线程池,它在创建的时候,没有核心线程,线程最大数量是Integer最大值,最大空闲时间是60S
FixedThreadPool:固定长度的线程池,它的最大线程数等于核心线程数,此时没有最大空闲时长为0
SingleThreadPool:单个线程的线程池,它的核心线程和最大线程数都是1,也就是说所有任务都串行的执行
ScheduledThreadPool:可调度的线程池,它的最大线程数是Integer的最大值,默认最长等待时间是10S,它是一个由延迟执行和周期执行的线程池
corePoolSize,maximumPoolSize,workQueue之间关系。
线程池执行流程 : 核心线程 => 等待队列 => 非核心线程 => 拒绝策略
一个成任务在线程池中是如何执行的?生活举例:
CorePoolSize:核心线程数,它是不会被销毁的
MaximumPoolSize :最大线程数,核心线程数+非核心线程数的总和
KeepAliveTime:非核心线程的最大空闲时间,到了这个空闲时间没被使用,非核心线程销毁
Unit:空闲时间单位
WorkQueue:是一个BlockingQueue阻塞队列,超过核心线程数的任务会进入队列排队
ThreadFactory:它是一个创建新线程的工厂
Handler:拒绝策略,任务超过最大线程数+队列排队数 ,多出来的任务该如何处理取决于Handler
拒绝策略,当线程池任务超过 最大线程数+队列排队数 ,多出来的任务该如何处理取决于Handler
可以定义和使用其他种类的RejectedExecutionHandler类来定义拒绝策略。
考虑CPU核数,并发数量综合来考虑
这是带定时任务的线程池,EurekaClient拉取注册表&心跳续约就是使用的这个线程池。
JDK1.6前
基于JVM底层,基于操作系统的锁,涉及用户态到内存态的切换性能很差
JDK1.6后
Synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁
索引是用来高效获取数据的存储结构如同字典的目录一样,数据库的索引通常使用b+tree来实现,索引树的节点和数据地址相关联,查询的时候在索引树种进行高效搜索,然后根据数据地址获取数据。索引提高了搜索的效率同时增加了索引维护的成本,滥用索引也会降低insert,update,delete的性能。
普通索引:允许重复的值
唯一索引:不允许有重复的值
主键索引:数据库自动为我们的主键创建索引,如果我们没有指定主键,它会根据没有null的唯一索引创建主键索引,否则会默认根据一个隐藏的rowId作为主键索引
全文索引,用来对文本域进行索引,比如text,varchar,只针对MyISAM有效
B+树和hash,Myisam和innodb都不支持hash
采用了B+树的数据结构,采用B+树的原因,B+树是多叉树,适合存储大量数据,B+树的数据存储在叶子节点,内部节点只存键值,因此B+树每次查询都要走到叶子节点, 查询性能更稳定,同时它的非叶子节点只存储key,因此每个节点能存储更多的key,树的高度变的更低,查询性能更快,而且它的叶子节点能够形成一个链表,支持范围查询,排序 。
他们都是用的B+树,不同的是
innodb的叶子节点存放的是数据,myisam的叶子节点存放的是数据的地址
innodb中辅助索引的叶子节点存放的是主键索引的键值,myisam中辅助索引的叶子节点存放的也是数据的地址
innodb的索引和数据都存放到一个文件中,myisam的索引和数据分别存放到不同的文件中
不经常查询的列不适合创建索引
不出现在where中的字段不适合创建索引
离散度太低的字段不适合创建索引,比如性别
更新非常频繁的字段不适合创建索引
模糊查询时,通配符放到左边的时候,会导致索引失效 比如 like ‘’%keyword%‘’
列是字符串类型,查询条件没有用引号,会导致索引失效
使用了or,in,not in,not exist, !=等,会导致索引失效
查询null值,会导致索引失效
还有mySQL认为全表扫描会比索引查找快,就不会使用索引,比如表里只有一条记录
除了主键索引之外的其他索引都叫辅助索引,也叫二级检索。辅助索引的叶子节点存储的是主键索引的键值,因此辅助索引扫描完之后还会扫描主键索引,这也叫回表
但是如果查询的列恰好包含在辅助索引的键值中,就不会再回表了,这也叫覆盖索引
InnoDB辅助索引的叶子节点存放的是,主键索引的键值
因此辅助索引扫描完还会扫描主键索引,也叫回表
但是如果查询的列恰好包含在辅助索引的键值中,就不会再回表了,这也叫覆盖索引
组合索引向左匹配,我们应该优先选择组合索引,因为对覆盖索引命中率更高,查询性能更高,但是应该考虑列的顺序,因为组合索引会向左匹配
不一定,比如:like “值%” 一样可以使用索引,向左匹配,而 like "%值"或 "_值"就不能命中索引。
查询较频繁的列应该考虑创建索引
不经常查询的列不适合创建索引
不出现在where中的字段不适合创建索引
离散度太低的字段不适合创建索引,比如性别
更新非常频繁的字段不适合创建索引
不合理的商业需求,比如实时更新总注册人数,总交易额等等,应该考虑不要实时
对于热点数据的查询并发太高,应该考虑用缓存
数据库结构设计不合理,比如几十个字段集中在一张表,应该考虑分表
SQL语句有问题,比如太多JOIN,很多不需要的字段也要全部查询出来,应该考虑优化SQL
硬件和网络方面的影响
客户端发起SQL查询,首先通过连接器,它会检查用户的身份,包括校验账户密码,权限
然后会查询缓存,如果缓存命中直接返回,如果没有命中再执行后续操作,但是MySQL8.0之后已经删除了缓存功能
接下来到达分析器,主要检查语法词法,比如SQL有没有写错,总共有多少关键字,要查询哪些东西
然后到达优化器,他会以自己的方式优化我们的SQL
最后到达执行器,调用存储引擎执行SQL并返回结果
不需要的字段就不要查询出来
小结果集驱动大结果集,将能过率更多数据的条件写到前面
in和not in尽量不要用,会导致索引失效
避免在where中使用or链接条件,这会导致索引失效
给经常要查询的字段建立索引
考虑如果不需要事务,并且主要查询的化,可以考虑使用MyISAM存储引擎
如果表数据量实在太庞大了,考虑分表
通过druid连接池的内置监控来定位慢SQL
通过MySQL的慢查询日志查看慢SQL
通过show processlist,查看当前数据库SQL执行情况来定位慢SQL
首先看一下硬件和网络层面,有没有什么异常
然后分析代码有没有什么问题,算法有没有什么缺陷,比如多层嵌套循环
最后我们再定位到慢SQL,比如
定位到慢SQL再考虑优化该SQL,比如说(explain)
如果优化SQL后还是很慢,可以考虑给查询字段建索引来提升效率
如果建立索引了还是慢,看一下是不是数据量太庞大了,应该考虑分表了
在SQL语句前加上explain,结果中的key就是实际用到的索引
主要有innodb,memory,myisam
innodb支持事务,速度相对较慢,支持外键,不支持全文索引
myisam 速度相对较快,支持全文索引,不支持外键,不支持事务,
memory不支持事务,基于内存读写,速度快,支持全文索引
如果对事务要求不高,而且是查询为主,考虑用myisam
如果对事务要求高,保存的都是重要的数据,建议使用innodb,它也是默认的存储引擎
如果数据频繁变化的,不需要持久化,可以使用memory
一个sql : select sum(amount) from recharge ,来查询总充值,recharge 表数据量达到了上千万,怎么优化
可以考虑建个汇总表来统计总充值,总订单数,总人数等等等
或者采用日报表,月报表,年报表,使用定时任务进行结算的方式来统计
或者看数据能不能使用ES搜索引擎来优化,如果非得要在这个上千万的表中来查询,那就采用分表
一组对数据库的操作,把这一组看成一个再给你,要么全部成功,要么全部失败。
举个栗子,比如A向B转账,A账户的钱少了,B账户的钱就应该对应增加,这就转账成功了,如果A账户的钱少了,由于网络波动等因素转账失败了,B账户的钱没有增加,那么A账户就应该恢复成原先的状态
原子性:指的是一个事务应该是一个最小的无法分割的单元,不允许部分成功部分失败,只能同时成功,或者同时失败
持久性:一旦提交事务,那么数据就应该持久化,保证数据不会丢失
隔离性:两个事务修改同一个数据,必须按顺序执行,并且前一个事务如果未完成,那么中间状态对另一个事务不可见
一致性:要求任何写到数据库的数据都必须满足预先定义的规则,它基于其他三个特性实现的
通过undo log 保证事务的原子性,redo log保证事务的持久性
undo log是回滚日志,记录的是回滚需要的信息,redo log记录的是新数据的备份
当事务开始时,会先保存一个undo log,再执行修改,并保存一个redo log,最后再提交事务。如果系统崩溃数据保存失败了,可以根据redo log中的内容,从新恢复到最新状态,如果事务需要回滚,就根据undo log 回滚到之前的状态
脏读:事务A读到了事务B修改还未提交的数据
幻读,也叫虚读:事务A两次读取相同条件的数据,两次查询到的数据条数不一致,是由于事务B再这两次查询中插入或删除了数据造成的
不可重复读:事务A两次读取相同条件的数据,结果读取出不同的结果,是由于事务B再这两次查询中修改了数据造成的
第一类丢失更新:也叫回滚丢失,事务A和事务B更新同一条数据,事务B先完成了修改,此时事务A异常终止,回滚后造成事务B的更新也丢失了
第二类丢失更新:也叫覆盖丢失,事务A和事务B更新同一条数据,事务B先完成了修改,事务A再次修改并提交,把事务B提交的数据给覆盖了
读未提交:事务读不阻塞其他事务的读和写,事务写阻塞其他事务的写但不阻塞读,能解决第一类丢失更新的问题,
读已提交:事务读不会阻塞其他事务读和写,事务写会阻塞其他事务的读和写,能解决第一类丢失更新,脏读的问题
可重复读:事务读会阻塞其他事务的写但不阻塞读,事务写会阻塞其他事务读和写,能解决第一类丢失更新,脏读,不可重复读,第二类丢失更新问题
串行化:使用表级锁,让事务一个一个的按顺序执行,能解决以上所有并发安全问题
利用了undo log实现的
undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,就可以利用undo log中的信息将数据回滚到修改之前的样子
利用了redo log实现的
redo log记录的是新数据的备份,在事务提交前,需要将Redo Log持久化,当系统崩溃时,可以根据redo Log的内容,将所有数据恢复到最新的状态
假设有A=1,B=2,两个数据,现在有个事务把A修改为3,B修改为4,那么事务的执行流程:
当事务开始时,会首先记录A=1到undo log,记录A=3到redo log,和记录B=2到undo log,记录B=4到redo log,然后再将redo log写入磁盘,最终事务提交
第一类丢失更新:也叫回滚丢失,事务A和事务B更新同一条数据,事务B先完成了修改,此时事务A异常终止,回滚后造成事务B的更新也丢失了
第二类丢失更新:也叫覆盖丢失,事务A和事务B更新同一条数据,事务B先完成了修改,事务A再次修改并提交,把事务B提交的数据给覆盖了
SQL标准中的四种隔离级别,读未提交,读已提交,可重复读,串行化,都能解决第一类数据更新丢失问题
对于第二类丢失更新问题,可以使用悲观锁也就是串行化来解决,也可以使用乐观锁的方式,比如加一个版本号管理来解决
隔离的实现主要利用了读写锁和MVCC机制
读写锁,要求在每次读操作时需要获取一个共享锁,写操作时需要获取一个写锁。共享锁之间不会产生互斥,共享锁和写锁,写锁与写锁之间会产生互斥。当产生锁竞争时,需要等一个操作的锁释放,另一个操作才能获得锁
MVCC,多版本并发控制,它是在读取数据时通过一种类似快照的方式将数据保存下来,不同的事务看到的快照版本是不一样的,即使其他事务修改了数据,但是对本事务仍然是不可见的,它只会看到第一次查询到的数据
可重复读是只在事务开始的时候生成一个当前事务全局性的快照,而读提交则是每次执行语句的时候都重新生成一次快照
MySQL主从同步,主负责写,从负责读,使用一主多从,能减轻读的压力
但是这不能解决写的压力和主库的单点故障,如果主库的写并发高,可以做成多个主库
主要依靠binlog来实现的,它记录的是所有的DDL,DML,TCL操作
当主库的数据发生改变时,会将改变记录保存到binlog中
从库新开一个线程将binlog内容发送到从库
从库会发起一个I/O线程请求主库的binlog,并保存到中继日志中
从库新开一个SQL线程,读取中继日志并解析成具体操作,从而将主库更新的内容写到了从库中
安装mySQL主从客户端,并配置my.ini
主库需要配置授权从库使用的账号和权限,启动后可以通过show 主库名 status查看状态,我们需要记录File和Position的值,File是对应的binlog文件名,position是当前同步数据的最新行
从库需要配置主库链接信息,包括账号密码和binlog文件名和最新行,然后启动。通过show 从库名 status 检查同步状态,Slave_IO_Running 和 Slave_SQL_Running 的值都为YES,说明大功告成了
垂直分表,可以理解为按列分表,如果一个表的字段太多了,可以按照使用频率分成不同的表,优化查询性能。比如商品表可以分为商品类型表,商品详情表,商品促销表等等
垂直分库,为了减轻单个数据库压力,我们可以按照业务类型,拆分成多个数据库,比如分布式架构,不同的模块可以有不同的数据库
水平分表,可以理解为按行分表,如果一个表的数据有千万行,查询性能太低,可以拆分成10张小表,每张表保存一百万行数据
水平分库,我们做了水平分表后,表数量太多了也会影响数据库查询效率,我们可以将这些表分到多个数据库中
会产生分布式事务,以前本地事务就能结局的问题现在要用上Seata分布式事务
垂直分库后跨库查询会导致一个查询结果来源于两个库,可能要用到多线程调用多个库查询
水平分库后一个分页查询的某一页可能来自两个库,可以将两个库的数据合并之后再执行SQL
水平分表后不同的表出现主键重复,可以通过雪花算法来解决
两个库都用到同一个表,那这个公共表的维护可能要用到MySQL主从同步
使用的是sharding-jdbc来实现的,它是由java开发的关系型数据库中间件,读写分离,分库分表操作简单
TDDL,淘宝业务框架,复杂而且分库分表的部分还没有开源
Mycat,要安装额外的环境,不稳定用起来复杂
MySQL官方提供的中间件,不支持大数据量的分不分表,性能较差
垂直分库,按照业务进行垂直分库,比如课程表和用户表放到不同数据库
垂直分表,把多字段表拆分少量字段表,比如将课程表分为课程类型表,课程详情表,课程促销表等
水平分表,把海量数据表拆分为多个小表
把商品业务进行水平分库,可以对水平分库后每一个数据库服务器进行集群
如果是并发高,可以考虑缓存,如果是数据量大可以考虑分库分表,具体如下:
首先应该考虑垂直分库,不同的业务使用不同的数据库
然后进行垂直分表,按照使用频率把字段多的表拆分成若干个表
对经常查询的列建立索引,提高查询效率
设计冗余字段,减少join表的次数
SQL优化,比如尽量使用索引查询
对热点数据应该考虑做缓存,比如首页展示汇总数据
从海量数据中查询数据应该考虑用全文检索
如果查询并发高,可以对mySQL做集群
如果数据量实在太大了,可以考虑水平分表,
水平分表后,表数量还是太多了,可以考虑水平分库
一主一从;一主多从;双主;环形多主;级联同步
可以考虑做集群,比如一主多从模式,然后对应用做读写分离
分表,分库,主从同步
垂直分表,可以理解为按列分表,如果一个表的字段太多了,可以按照使用频率分成不同的表,优化查询性能。比如商品表可以分为商品类型表,商品详情表,商品促销表等等
垂直分库,为了减轻单个数据库压力,我们可以按照业务类型,拆分成多个数据库,比如分布式架构,不同的模块可以有不同的数据库
水平分表,可以理解为按行分表,如果一个表的数据有千万行,查询性能太低,可以拆分成10张小表,每张表保存一百万行数据
水平分库,我们做了水平分表后,表数量太多了也会影响数据库查询效率,我们可以将这些表分到多个数据库中
按照区间范围分表,比如把用户按照年龄分为新生代表,青年代表,老年代表
按照时间分表,比如按照年来分表,比如登录日志,分成今年的表,去年的表。。
hash分表,通过将某一列的值比如id,通过一定的hash算法来算出对应那张表
雪花算法,通过雪花算法生成id,根据id来算出对应那张表
首先导入相关的依赖
然后在配置文件中配置datasource,包括主从数据库的名字,主从数据库的连接信息,配置负载均衡
项目中就可以正常使用datasource了,自动做读写分离
首先,要改造数据库,比如水平分表,水平分库
在配置文件中,需要做如下配置
datasource名字,多个数据源就配多个datasource
分库策略,比如按照哪一列分库,分库规则
分表策略,比如哪些库下面的哪些表,按照那一列分表,分表规则
配置公共的表
然后项目中就可以正常使用了
优点是读写分离,分担了读的压力。同时能起到备份作用,防止数据丢失。
缺点是不能分担写的压力,主的单点故障没有解决,存储没有得到扩容。
当主服务器中断服务后,可以将一个从服务器升级为主服务器 ,以便继续提供服务
哨兵就是用来监控主从服务器,实现故障恢复功能的。它会不断的检查主服务器和从服务器的健康状态,当某个服务器出现问题时,可以向管理员发起通知。如果主服务器不可用时,会自动选择一个从服务器作为新的主服务器,并让其他的从服务器从新的主服务器复制数据
哨兵也是主从模式,没有解决写的压力,只减轻了读的压力,而且存储也得不到扩容
Redis Cluster集群采用哈希槽 (hash slot)的方式来分配的。它默认分配了16384个槽位,当我们set一个key 时,会用CRC16算法得到所属的槽位,然后将这个key 分到对应区间的节点上
Redis Cluster有一个容错机制,如果半数以上的主节点与故障节点通信都超时了,就会认为该节点故障了,自动触发故障转移操作,故障节点对应的从节点升级为主节点。
但是如果某个主节点挂了,又没有从节点可以使用,那么整个Redis集群就不可用了、
简单动态字符串,是Redis自己封装的字符串结构。它记录了字节数组buf,字节数组中用到的字节数len,以及未使用的字节数free。
ES的索引库由多个分片 shard组成,shard分为primary shard主shad和replica shard 副本,主shard承担写请求,replica副本的数据从primary复制而来,同时分担读请求,primary shard的数量设定了就不能修改,replica数量可以修改。
(1) 客户端请求一个协调节点coordinating node
(2) 协调节点根据算法选择一个primary shard: 算法 hash(document_id) % (num_of_primary_shards)
(3) 对应的primary shard 所在节点保存完数据后,将数据同步到replica node。
(4) 协调节点coordinating node 发现 primary node 和所有 replica node 都搞定之后返回结果给客户端
(1) 当分片所在的节点接收到来自协调节点的请求后,会将请求写入到Memory Buffer,然后定时(默认是每隔1秒)写入到Filesystem Cache,这个从Momery Buffer到Filesystem Cache的过程就叫做refresh
(2) 当然在某些情况下,存在Momery Buffer和Filesystem Cache的数据可能会丢失,ES是通过translog的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到translog中,当Filesystem cache中的数据写入到磁盘中时,才会清除掉,这个过程叫做flush;
(3)在flush过程中,内存中的缓冲将被清除,内容被写入一个新段,段的fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的translog将被删除并开始一个新的translog。
flush触发的时机是定时触发(默认30分钟)或者translog变得太大(默认为512M)时;
(1) 客户端请求一个协调节点coordinating node
(2) coordinate node 根据算法hash(document_id) % (num_of_primary_shards),将请求转发到对应的 node,此时会使用 round-robin随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡
(3) 接收到请求的 node 返回 document 给调节点 coordinate node。
(4) coordinate node 返回 document 给客户端。
搜索被执行成一个两阶段过程,我们称之为 Query Then Fetch;
(1) 在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。
(2) 每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。PS:在搜索的时候是会查询Filesystem Cache的,但是有部分数据还在Memory Buffer,所以搜索是近实时的。
(3) 每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,协调节点它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
(4) 接下来就是 取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并 丰富 文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。
删除和更新也都是写操作,但是Elasticsearch中的文档是不可变的,因此不能被删除或者改动以展示其变更; 磁盘上的每个段都有一个相应的.del文件。当删除请求发送后,文档并没有真的被删除,而是在.del文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del文件中被标记为删除的文档将不会被写入新段。
在新的文档被创建时,Elasticsearch会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。
分为主节点,node.master =true , 数据节点node.data =true , 负载均衡节点(node.data =false,node.master=false),
node.master=true,代表该节点有成为主资格 ,主节点的主要职责是和集群操作相关的内容,如创建或删除索引,跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点。一般会把主节点和数据节点分开
node.data=true,数据节点主要是存储索引数据的节点,主要对文档进行增删改查操作,聚合操作等,数据节点对CPU,IO,内存要求较高,优化节点的时候需要做状态监控,资源不够时要做节点扩充
当主节点和数据节点配置都设置为false的时候,该节点只能处理路由请求,处理搜索,分发索引操作等,从本质上来说该客户节点表现为智能负载平衡器。配置:mode.master=false,mode.data=false
绿色,黄色,红色,绿色代表集群健康,所有的主备分片都得到分配,如果有备分片没有node去分配,集群是黄色,黄色和绿色都是可用状态,如果有主分片的节点down机,集群不可写数据,呈现红色,代表集群不健康。
jconsule, jvisualvm
loading加载:class文件从磁盘加载到内存中
verification验证:校验class文件,包括字节码验证,元数据验证,符号引用验证等等
preparation准备:静态变量赋默认值,只有final会赋初始值
resolution解析:常量池中符号引用,转换成直接访问的地址
initializing初始化:静态变量赋初始值
BootStrap ClassLoader 启动类加载器,加载
Extenstion ClassLoader 扩展类加载器,加载
Application ClassLoader 应用程序类加载器,加载Classpath下的类
自定义类加载器
这里是用到了双亲委派模式,从上往下加载类,在这过程中只要上一级加载到了,下一级就不会加载了,这麽做的目的
运行时数据区:
堆:存放对象的区域,所有线程共享
虚拟机栈:对应一个方法,线程私有的,存放局部变量表,操作数栈,动态链接等等
本地方法栈:对应的是本地方法,在hotspot中虚拟机栈和本地方法栈是合为一体的
程序计数器:确定指令的执行顺序
方法区:存放虚拟机加载的类的信息,常量,静态变量等等,JDK1.8后,改为元空间
执行引擎:
即时编译器,用来将热点代码编译成机器码(编译执行)
垃圾收集,将没用的对象清理掉
本地方法库:融合不同的编程语言为java所用
线程执行,每个方法都会形成一个栈帧进行压榨保存到虚拟机栈中,方法调用结束就回出栈。调用过程中创建的变量在虚拟机栈,对象实例存放在堆内存中,栈中的变量指向了堆中的内存。当方法执行完成就出栈,创建的变量会被销毁,堆中的对象等待GC。
增加启动参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:\ 可以把内存溢出的日志输出到文件,然后通过JVM监视工具VisualVM来分析日志,定位错误所在。在linux服务器也可以使用命令: jmap -dump 来下载堆快照。
垃圾标记算法有:引用计数和可达性算法
有一个地方引用它时,计数器值加1
;每当有一个地方不再引用它时,计数器值减1
,这样只要计数器的值不为0,就说明还有地方引用它,它就不是无用的对象. 这种算法的问题是当某些对象之间互相引用时,无法判断出这些对象是否已死标记清除算法 :分为标记和清除两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象 ;缺点:标记和清除两个过程效率都不高;标记清除之后会产生大量不连续的内存碎片。
复制算法 :把内存分为大小相等的两块,每次存储只用其中一块,当这一块用完了,就把存活的对象全部复制到另一块上,同时把使用过的这块内存空间全部清理掉,往复循环 ,缺点:实际可使用的内存空间缩小为原来的一半,比较适合
标记整理算法 :先对可用的对象进行标记,然后所有被标记的对象向一段移动,最后清除可用对象边界以外的内存
分代收集算法 :把堆内存分为新生代和老年代
,新生代又分为Eden区、From Survivor和To Survivor。一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此新生代采用复制算法
,只需要复制那些少量存活的对象就可以完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法
来进行回收。
新生代:Serial :一款用于新生代的单线程收集器,采用复制算法进行垃圾收集
。Serial进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World
新生代:ParNew : ParNew就是一个Serial的多线程版本`,其它与Serial并无区别。ParNew在单核CPU环境并不会比Serial收集器达到更好的效果,它默认开启的收集线程数和CPU数量一致,可以通过-XX:ParallelGCThreads来设置垃圾收集的线程数。
新生代:Parallel Scavenge(掌握) Parallel Scavenge也是一款用于新生代的多线程收集器
,与ParNew的不同之处是,ParNew的目标是尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge的目标是达到一个可控制的吞吐量
.Parallel Old收集器以多线程,采用标记整理算法进行垃圾收集工作。
老年代:Serial Old ,Serial Old收集器是Serial的老年代版本,同样是一个单线程收集器,采用标记-整理算法。
老年代CMS收集器是一种以最短回收停顿时间为目标的收集器,以“最短用户线程停顿时间”著称。整个垃圾收集过程分为4个步骤
用标记-清除算法清除垃圾对象
,耗时较长整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作,所以从总体上来说,CMS收集器垃圾收集可以看做是和用户线程并发执行的。
老年代:Parallel Old ,Parallel Old收集器是Parallel Scavenge的老年代版本,是一个多线程收集器,采用标记-整理算法。可以与Parallel Scavenge收集器搭配,可以充分利用多核CPU的计算能力
。
堆收集:G1 收集器, G1 收集器是jdk1.7才正式引用的商用收集器,现在已经成为jdk1.9默认的收集器
。前面几款收集器收集的范围都是新生代或者老年代,G1进行垃圾收集的范围是整个堆内存
,它采用“化整为零”的思路,把整个堆内存划分为多个大小相等的独立区域(Region)
在每个Region中,都有一个Remembered Set来实时记录该区域内的引用类型数据与其他区域数据的引用关系(在前面的几款分代收集中,新生代、老年代中也有一个Remembered Set来实时记录与其他区域的引用关系),在
标记时直接参考这些引用关系就可以知道这些对象是否应该被清除,而不用扫描全堆的数据
Jdk1.7.18新生代使用Parallel Scavenge,老年代使用Parallel Old
新生代的回收称为Minor GC,新生代的回收一般回收很快,采用复制算法,造成的暂停时间很短 ,而Full GC一般是老年代的回收,并伴随至少一次的Minor GC,新生代和老年代都回收,而老年代采用
标记-整理算法,
这种GC每次都比较慢,
造成的暂停时间比较长`,通常是Minor GC时间的10倍以上。尽量减少 Full GC
优化程序的内存使用大小,以及减少CG来减少程序的停顿来提升程序的性能。
-Xms : 初始堆,1/64 物理内存
-Xmx : 最大堆,1/4物理内存
-Xmn :新生代大小
-Xss : 栈大小
一个类只能有一个实例,主要用于需要频繁使用的对象避免频繁初始化和销毁来提高性能,或者资源需要相互通信的环境
主要实现方式有,饿汉模式,懒汉模式,枚举,静态内部类
饿汉模式,是在类加载过程中就将这个单例对象实例化,需要将构造方法私有化,定义一个成员变量并new一个该类的实例作为初始值,提供一个公共的静态方法获取这个实例
懒汉模式,是在使用时才创建这个单例对象,需要将构造方法私有化,定义一个该类的成员变量不赋初始值,提供一个获取实例的公共静态方法。特别注意这个方法需要保证多线程环境下的并发安全性,可以通过DCL加volatile关键字来解决
枚举,直接在枚举中定义字段,它就是单例并且线程安全的
静态内部类,在类中搞一个静态内部类,在静态内部类中搞一个目标类的静态成员变量并且new一个实例作为初始值。然后在目标类中定义一个获取实例的静态方法,方法返回的就是静态内部类中的成员变量。这种方式能保证线程安全,也能实现延迟加载。缺点是这种方式传参不太方便
定义一个算法骨架,而将某个或多个具体的实现延迟到子类中,使得子类可以在不修改当前算法的结构情况下,重新定义当前算法的某些特定步骤
比如考试中所有考生的试卷都一样,答案由每个考生自己完成
将不兼容的接口转换为可兼容的接口的中间类
比如HandlerInterceptorAdapter ,我们定义拦截器时不需要覆写HandlerInterceptor中的所有方法,因为适配器类帮我们做了空实现。但JDK1.8之后,给接口中增加了默认方法,可以有方法体,因此这些适配器类已经失去作用了
不直接使用实际对象,通过调用代理对象间接调用实际对象,主要用作对实际对象的增强,分为静态代理,JDK动态代理(基于接口的动态代理),CGLIB动态代理(基于类的动态代理)。
动态代理举例
举个例子,小明(真实角色)的主要业务是唱歌,在还没火的时候自己跑东跑西去街头、酒吧等地方唱歌,在这期间需要自己负责找合适的场地,以及和酒吧老板谈工资等业务,突然有一天他火了,很多人要请他唱歌,所有的事亲历亲为他根本忙不过来,这个时候经纪人(代理角色)出现了,客户们要请小明唱歌,不能直接找到小明了,而是需要和经纪人谈,经纪人在商业等方面的理解比小明强(对代理对象的方法增强)。这样小明就可以专注于自己的唱歌业务了,经纪人的存在就是拦截了客户对小明(真实对象)的访问。
相信通过上面这个例子,大家能够理解动态代理了。我们在做项目的时候把日志和异常统一处理就是利用了动态代理,让我们能够关注核心业务。
JDK动态代理是jdk提供的,我们可以直接使用,而CGLIB需要导入第三方库
JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用目标方法前调用InvokeHandler来处理
CGLIB动态代理是先加载目标类的class文件,然后修改其字节码生成子类来实现的
单例模式:一个类只能有一个实例,分为饿汉模式(迫切加载)和懒汉模式(延迟加载)和枚举。
工厂模式:隐藏了产品的复杂创建过程,实现生产功能的复用,让产品生产更加高效。分为简单工厂(需要来回切换生产线),工厂方法(开设新的生产线),抽象工厂(制定创建产品的接口,让子工厂选择创建哪种产品)
在Spring中各种的BeanFactory创建bean都用到了
模板模式:定义一个算法骨架或者算法的流程,而不同的实例实现方式不同,将某个或多个具体的实现延迟到子类中,比如RedisTemplate实现了RedisOperations,ElasticSearchTemplate实现了ElasticsearchOperations
代理模式:不直接使用实际对象,通过调用代理对象间接调用实际对象,主要用作对实际对象的增强,分为静态代理,JDK动态代理,CGLIB动态代理比如Spring的AOP原理就是动态代理,当目标对象实现了接口会使用JDK动态代理,没有实现接口会使用CGLIB动态代理
适配器模式:将不兼容的接口转换为可兼容的接口的中间类,比如HandlerInterceptorAdapter ,我们定义拦截器时不需要覆写HandlerInterceptor中的所有方法,因为适配器类帮我们做了空实现。但JDK1.8之后,给接口中增加了默认方法,可以有方法体,因此这些适配器类已经失去作用了
观察者模式:当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,比如Spring中的ApplicationListener
存数据及数据之间关系集合。
数据结构就是计算机存储、组织数据的方式
按照逻辑结构分
按照物理结构分
顺序存储结构:用一组地址连续的存储空间依次存储线性表的数据元素,也叫顺序存储结构,比如数组
链接存储结构:用一组任意的存储空间来存储线性表中的数据元素,不要求相邻元素在物理位置上也相邻,比如链表
数据索引存储结构:建立附加的索引来标识节点的地址,通过索引,可以很快检索数据
数据散列存储结构:将数据元素的存储位置与关键字之间建立确定的对应关系,加快查找的速度,又叫hash存储
数组在内存中是一组连续的存储空间,它随机存取元素性能很高,但是插入和删除操作,需要移动其他元素,因此性能很低
链表在内存中的存储空间可以是不连续的,而在每一个元素中都保存相邻节点的指针,因此它的存储密度相对较小,查找的性能低,因为需要从第一个元素依次遍历,但是它的插入和删除操作性能很高,因为它不需要移动节点,只需要改变相邻节点指针就行了,同时它更容易造成内存的碎片化
散列存储,它通过把关键码的值映射到表中的一个位置,来提高查询的速度。而这个映射函数叫做散列函数。
哈希冲突,也叫哈希碰撞,指的是两个不同的值,计算出了相同的hash,也就是两个不同的数据计算出同一个下标,通常解决方案有:
拉链法,把哈希碰撞的元素指向一个链表
开放寻址法,把产生冲突的哈希值作为值,再进行哈希运算,直到不冲突
再散列法,就是换一种哈希算法重来一次
建立公共溢出区,把哈希表分为基本表和溢出表,将产生哈希冲突的元素移到溢出表
时间复杂度是用来度量算法执行的时间长短,通常我们用O(f(n))渐进时间复杂度来衡量,比如说
数组:按照顺序物理结构存储,ArrayList
链表:按照链式物理结构存储,LinkedList
栈:LIFO后进先出的线性存储结构,分为用数组实现的顺序栈,用链表实现的链栈
队列:FIFO先进先出的线性存储结构,分为顺序队列和链式队列
串:特殊的线性存储结构,String,StringBuffer,StringBuilder
线性结构,对于大量的输入数据,访问时间很长,效率很低,树形结构的优势在于它查找数据性能很高
树有二叉树,多叉树,他们特点如下
二叉树:树中任意节点最多只有两个分叉的树,它又分为二叉排序树,平衡二叉树,赫夫曼树,红黑树
二叉排序树,它是一个有序的二叉树,优势在于查找插入数据的性能很高,但是可能会出现倾斜而变成数组
平衡二叉树,二叉排序树进化形态,要求任何节点的两颗字数高度差不大于1。它的查询性能很高,但是每次增删元素,会重排序导致性能低
红黑树,自平衡二叉树,要求根节点和叶子节点是黑色,其他节点红黑交替,在任何一个子树中,从根节点向下走到空姐点的路径经过的黑节点数相同。从而保证了平衡。它的查询性能比平衡二叉树稍低,插入和删除元素的性能大幅提高。
多叉树:解决二叉树存储大规模数据时,深度过大而导致IO性能低,查询效率低的问题,常见有B树和B+树,字典树,后缀树等等
B树,自平衡的树,一个节点可以存储多个key,和拥有key数量+1个分叉,适用于读写相对大的数据块,比如文件系统,数据库索引。因为相对二叉树来说,节点存储key越多,分叉越多,需要的节点越少,树高越矮,IO次数少,查询效率越高。
B+树,B树升级版,它的内部节点只存储key,不存储具体数据,叶子节点存放key和具体数据。这就使得每个节点可以存更多的key,树的高度更低,查询更快,同时它每次查询都会到叶子节点,查询速度更稳定。并且所有的叶子节点会组成一个有序链表,方便区间查询
因为二叉树在大规模的数据存储中,树会高的没谱,这会导致IO读写过于频繁,查询效率低下
多叉树可以解决这个问题,它每层可以存放更多的数据,因此能大幅度降低树的深度,提高查询性能
一是节点存储内容上的区别:B树每个节点都可以存放key,存放数据,而B+树所有内部节点只存放key,叶子节点存放key和数据,因此它的节点能存放更多数据,降低树高,查询性能更快
二是B+树所有的叶子节点会构成一个链表结构,方便区间查找和排序
ES是使用了数据索引存储结构,它是通过为关键字建立索引,通过索引找到对应的数据,这种索引也叫倒排索引,可以实现快速检索
一:常见数据结构
存数据及数据之间关系集合。
数据结构就是计算机存储、组织数据的方式
1)集合
2)线性结构
3)树状结构
4)图形结构
1)顺序存储 - java用数组
2)链式存储
3)索引存储 Es
4)hash存储
1)集合 HashSet
2)线性结构
数组
线性表-顺序表(ArrayList)链表(LinkedList)
栈:FILO 顺序栈 链栈
队列:FIFO 顺序队列 链式队列
串:定长String 堆StringBuilder StringBuffer
3)树状结构
二叉树:二叉排序树。。。红黑树。。。
红黑树:平衡二叉树,相同的数据,红黑树的树高更低(为了保证树是一颗平衡二叉树,增加了null节点,通过增加null节点,减少为了保证平衡而做的大量数据调整) 多叉树:B树 B+树(innodb)
在二叉树的基础上,数据量增大之后,为了保证各种效率,需要对数据做持久化,那么就需要用到合适和数据结构来存
1.相同的树高,B+树会比B树存储的数据多得多
2.B+数的查询效率稳定
3.所有数据都是有序存储到叶子节点的,可以使用算法快速搜索
解决问题步骤有限集合。 在java中就是方法
时间复杂度:时间频度,从理论上看它执行多少语句 o(1) o(n) 。。。 o(N平方)
空间复杂度:是否有额外空间
空间换取时间
hash(散列)算法
hash冲突
解决hash冲突:拉链表,开放寻址,再散列,简历公共溢出区
递归
递归:自己调用自己,有出口
递归优化-数据库查询
排序:
java Arrays.sort Collections.sort Compartor(小于返回负数,大于返回正数)
还要很多排序算法 冒泡。。。
查找
二分查找:排序数据
put值 put(“laoyang”,“你好啊”);
1.使用hash函数对key。计算hash值 hash(“laoyang”)=35
2.在根据散列值模数组长度 35/16=33.找到3对应的数组下标位置,判断
3对应的数组下标位置,是否已经存在元素了
3.1 如果不存在,直接放入数组位置即可 Node
3.1.1 如果放入元素后,发现数字的内容已经装了超过0.75负载因子(12个),会执行数组自动扩容–翻倍
3.2 如果已经存在值,去看可以是否一致
3.2.1 如果key一致,直接覆盖value
3.2.2 如果key不一致,使用拉链表的方是来存储Node数据
4.如果链表的长度超过8,自动将链表转换为红黑树
5.如果红黑树的节点树小于
6,自动将红黑树转换为链表
get一个值get(“laoyang”)
1.使用hash函数对key。计算hash值 hash(“laoyang”)=35
2.在根据散列值模数组长度 35/16=3
3.找到3对应的数组下标位置,判断3对应的数组下标位置,是否只有一个node
3.1 只有一个,直接返回value给你
3.2 不止一个,需要判断类型是链表还是红黑树
3.2.1 链表拿值 循环遍历链表,拿到每一个节点Node,比较key和需要搜索的key是否一致,如果一致,说明找到了,将value返回 如果key不一致,找下一个,直到遍历完,如果都没有,那就返回null
tomcat是一个开源而且免费的jsp服务器,属于轻量级应用服务器。它可以实现JavaWeb程序的装载,是配置JSP(Java Server Page)和JAVA系统必备的一款环境。
tomcat举例
后端使用zuul网关,请求先到达zuul网关,zuul做登录检查,zuul网关底层整合ribbon把请求路由到下游微服务,服务之间使用OpenFeign进行通信。执行成功后原路返回结果。
可以做zuul集群,使用Nginx做负载均衡到zuul集群,然后Nginx可以采用双机主备,或者双机互备做集群防止单点故障。如果并发非常高可以加上LVS做负载。
首先,可以设置图形验证码,流量错峰
其次,可以获取请求的ip地址,手机号,发送时间,并保存到发送短信记录的日志中,对于短时间多次请求的ip地址,手机号,可以拦截不执行发送手机验证码
再次,可以设置单位时间内发送短信的总数量,比如设定1秒最多只发送10条验证码。但这种方式会降低并发性
使用定时任务每日结算即可。把结算的数据保存到一个统计表中
数据量大的时候,定时任务扫描表性能会很差,而且多数都是空扫描,还有延迟问题,
对于我们的小型项目,可以使用quartz定时器,使用起来也很简单方便,但如果是高并发,比如秒杀等业务,可以使用RabbitMQ的延迟队列来实现,也可以使用Redis来做延迟队列。
MQ异步下单 , 秒杀系统,微服务授权,视频异步推流
RBAC:Role-Based Access Control首字母缩写,意为基于角色的访问控制。基本思想是对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。
将权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
实现RBAC,需要将用户对权限的多对多关系,转化为用户对角色,角色对权限的多对多关系,因此在数据库中,需要在用户,角色,权限中分别加入中间表,即用户表,用户和角色关系表,角色表,角色和权限关系表,权限表
MVVM,Model–View–ViewModel首字母缩写,是一种软件架构模式。
其中Model指的是模型,包括数据和一些基本操作
View指的是视图,页面渲染结果
ViewModel指的是模型与视图间的双向操作
MVVM的思想就是数据模型和视图的双向绑定,只要数据变化,视图会跟着变化,只要视图被修改,数据也会跟者变化
v-text:给元素填充纯文本内容
v-html:给元素填充内容,与v-text的区别是它会把内容的html符号进行渲染
v-for:遍历数字,字符串,数组,对象
v-bind:将data中的数据绑定到标签上,作为标签的属性,缩写是:,比如:src="srcFilePath"其中srcFilePath是一个会动态变化的值,一般从后端获取;
v-model:创建双向绑定,表单的值被修改时会自动修改data中的数据,data中的值变化时页面也会被修改
v-show:根据表达式的真假值,切换元素的css属性
v-if:根据表达式的真假值,销毁或重建元素
v-on:绑定事件,缩写:@,比如@click="doSomething"便是监听到点击事件后执行函数doSomething;
v-slot,插槽指令,缩写:#,一般不使用缩写,可用于接收值然后在子组件中使用。
VUE项目需要打包后才能部署
首先,它可以将ES6等高级语法,编译成各个浏览器都认识的语法
其次,它可以将相互依赖的许多散碎文件搞成一个整体,提高网页访问的效率
再次,它可以将代码压缩,减小代码体积
组件是一种自定义的元素标签,可以对功能封装,提高代码复用性,分为全局组件和局部组件两种
全局组件,是在所有vue挂载的标签中都有效,
局部组件,只在当前vue所挂载的标签中有效
基础组件,比如按钮Button,图标Icon
表单组件:比如表单Form,单选框Radio,多选框Checkbox,输入框Input,选择器Select,级联选择器Cascader
其他组件:比如Dialog对话框,消息提示Message
给保存在Redis中的token设置过期时间来处理登录过期的,为了防止已登录用户在访问后台时突然遭遇登录过期的情况,我们在后台接收到用户访问时,重新设置token的过期时间写入Redis,则用户访问期间就不会突然过期了
首先可以看出表的结构是自关联,三层的树状结构,分类表的字段可以有主键id,商品名,创建时间,修改时间,上架时间,下架时间,商品数量,排序,图标,父级id
首先,在entity中加入子分类字段children
查询方式有四种
第一,使用嵌套for循环,循环体内查询每一层级的数据,并关联到children。当然这也可以使用递归函数来实现
第二,使用mybatis的嵌套查询,也就是主查询加额外子sql查询的方式
第三,使用mybatis的嵌套结果,也就是join连表查询的方式
第四,只使用一次查询,将所有数据查询出来,通过一种算法来实现:除了第一级,其他所有数据都关联到自己的父级分类,结果返回第一级数据就可以
第一,第二种方式,当层级多的时候查询性能极低,第三种方式一般只能查询两层结构,第四种方式性能最高,适用于数据量本身并不大但层级很多的场景
所有课程的数据本身体量小,层级多,因此采用了第四种方式。
登录信息login,使用的是String结构存储
手机验证码code,使用的是String结构
课程分类course_type ,使用的是String结构
购物车保存,使用的是Hash结构
发布课程两大步
第一步,将课程的状态改为上线并保存到数据库中,
第二步,将课程信息保存到ES中,方便门户网站展示
我们按照字段的使用频次,垂直分表来设计,分为课程主表,课程详情表,课程类型表,课程市场详情表。
课程主表,包括主键id,课程名称,课程类型id,课程上下线状态,适用人群,课程等级,课程所属机构等,并且冗余了课程类型名,课程价格字段来提高前台的查询性能
课程详情表,包括课程简介,课程详情
课程市场详情表,包括课程价格,促销活动,活动过期时间
课程类型表,包括主键id,类型名,创建修改时间,课程数量,父级id
其中课程主表和课程详情表、课程主表和课程市场详情表,都是一对一的关系,他们采用相同的主键id来相互关联。课程主表和课程类型表是多对一的关系,在课程主表添加类型id来相互关联
我们项目分为两大版图,
入驻我们平台的培训机构,可以发布相关课程,入驻平台的企业,可以发布相关的就业招聘信息
门户网站的大众用户,可以选择培训机构发布的课程来进行学习,可以选择企业发布的招聘信息来就业
俺们项目是按照最高2000 QPS设计的,实际并发数运维在统计,俺也不太清楚
俺们项目都有分库分表,按服务拆分多个数据库,对于有些数据量大的表,我们也是按照字段的使用频率,拆分成多个表,比如课程表拆分成课程主表,课程详情表,课程分类表等等。
但是有些表比如日志,流水相关的表,数据量还是很大的,之前我知道是2000W多一点现在不知道多少。
首先,课程在发布的时候,就同时将课程信息存放到ES中,信息中包括了需要查询的字段,如课程标题,课程分类,课程等级,机构名,销量,浏览量,上线时间,价格等等
接下来,根据用户在前台发送的查询条件,在ES中搜索对应的课程,并作关键字高亮处理,排序和分页处理,然后返回前台
前后端分开部署,前端使用Nginx部署,
后端使用Springboot内嵌的tomcat部署,
分开部署后,通过代理解决前后端域名不一致的跨域问题
第一,专人干专事,前后端同时开发,效率更高
第二,责任分离,避免了前后端相互踢皮球的现象
第三,前后端解耦合,一套后端可以处理不同的前端,包括app端,浏览器端
第四,分开部署,减轻了服务器压力
第五,页面显示东西再多也不怕,数据都是异步加载,就算后端服务器挂了,前端页面也能访问,虽然没有数据
第六,前端分离出去,后端写一套接口就可以适用于web,app端
使用主流的Git管理项目
第一。Git是每个攻城狮都有自己的版本库,可以在自己的库上任意操作提交代码
第二。Git在每个工程只产生一个.git目录,而SVN会在每个目录下都生成.svn目录
第三。Git能快速切换分支,且合并文件的速度比SVN快
第四。Git采用分布式版本库,内容完整性更好
docker 容器 ,使用Jnekins做持续集成。
git clone:从远程仓库克隆项目到本地
git add:添加代码到本地仓库管理
git commit:提交add后的代码到本地仓库
git push:推送本地仓库文件到远程仓库
git pull:拉取远程仓库中的代码到本地仓库
使用crontab即可做到,如下配置:
crontab -e
0 0 * * 7 /bin/cp /user/backup /tmp
查看tomcat日志,使用tail命令tail -n N filename.txt
。n是查看行数
ps -ef | grep 软件名
使用 : tar -zcvf压缩 , tar -zxvf 解压缩
或者使用: zip 压缩成zip, unzip解压
单体应用的部署是比较简单的,前端打包上传使用Nginx,后端打包成war,可以使用Tomcat来部署,如果是SpringBoot的可以默认打包为jar,直接java -jar 启动。如果用到其他组件,比如Redis可以直接在服务器安装,然后项目指向其IP即可。
如果项目的组成部分比较多,比如:项目后端,前端,Redis,Mysql等等都涉及到,那么可以使用Docker来部署,这样更好管理应用之间的内存和资源分配。
我们微服务有20个服务器,业务系统是8核CPU,16G内存,有些服务配置还要低一些,视频处理系统是12核CPU,24G内存。通过NFS方式共享20T硬盘。
docker是一个容器技术,最大的好处是做资源的分配和管理,传统的linux部署项目不好管理内存等资源的分配,造成了应用之间资源竞争的情况,Dcoker的出现解决了这一问题。我们可以把我们的应用打包成Docker的镜像,然后启动成容器。容器和容器之间相互隔离也可以互相通信。就类似于有多个主机一样。
docker images :查看本地镜像
docker search : 搜索镜像
docker pull : 下载镜像
docker push : 上传镜像到仓库
docker rmi : 删除镜像
docker run : 创建并启动一个容器
docker ps : 查看容器
docker rm :删除容器
docker stop : 停止容器
docker kill :停止容器
docker start : 启动容器
docker exec -it 容器名 /bin/bash : 进入容器
docker exit :退出容器
docker cp : 拷贝文件到容器,或者从容器中拷贝文件到linux
docker logs : 查看容器的日志
docker cp 或者在启动容器的时候增加 -v 做目录映射
服务不可访问,那就是容器出问题了,我会去找到对应的容器是不是挂了,或者使用docker logs 查看日志根据错误日志来排错。
使用容器IP通信,但是容器重启IP会变动,不建议
使用端口映射也可以通信,但是内网部署的应用不需要做端口映射,所以这个不建议用
使用–link 名字进行通信
使用桥接网络通信
首先肯定要下载一个redis的镜像, 对于zuul的镜像可以使用docker插件对zuul进行打包。
redis是不需要暴露给外网的,所以不要做端口映射,可以使用–link或桥接网络通信 ,而zuul是服务访问入口需要做端口映射进行外网部署。