线程

线程的分类

主线程 main方法线程
守护线程
(1)设置为精灵线程的方法:setDaemon(true);
(2)其他线程结束了 精灵线程也完了
(3)又叫守护线程或者后台线程
子线程 new创建

线程的状态

准备、就绪、运行、阻塞、死亡
技术栈 - 图2

线程间的可见性

什么是线程间的可见性?
一个线程对共享变量值的修改,能够及时的被其他线程看到。

什么是共享变量?
如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。

创建线程的方法

1.继承Tread类,并覆盖run方法
1.自定义类MyThread继承Thread类。
2.MyThread类里面重写run()方法。
3.创建线程对象。
4.启动线程。
2.实现Runnable接口,实现run()方法
1.自定义类MyRunnable实现Runnable接口
2.重写run()方法
3.创建MyRunnable类的对象
4.创建Thread类的对象,并把步骤3创建的对象作为构造参数传递
5.启动线程
3.实现callable接口
1.创建 MyCallable 测试类实现 Callable接口,并指定返回类型;
2.重写 call() 方法;
3.使用FutureTask来获取 call() 的返回值;
4.使用get() 方法来获取执行结果时,该方法会产生阻塞,会一直等到任务执行完毕获取执行结果。
1.call()方法可以有返回值
2.call()方法可以声明抛出异常
4.通过线程池启动多线程
例如用Executor框架或者ThreadPoolExecutor

线程的常用方法

wait() 释放锁,让出cpu,进入等待
sleep() 使当前线程进入休眠状态 不释放锁 会造成死锁
notify() 唤醒等待队列中的第一个线程
notifyAll() 唤醒所有正在等待的其他线程
join() 等待当前线程执行结束
yield() 释放cpu执行权

进程和线程的区别

1.进程是运行中的程序,线程是进程的内部的一个执行序列。
2.进程是资源分配的单元,线程是执行单元。
3. 进程间切换代价大,线程间切换代价小。
4. 进程拥有资源多,线程拥有资源少。
5. 多个线程共享进程的资源。

线程池

线程池的状态

runing 准备就绪 可以添加任务以及对已经存在的任务进行执行
shutdown 不接受新任务 但是能处理已经存在的任务
stop 不接受新任务,也不处理已经创建的业务
tidying 当线程池的任务数量为0的时候线程池会变成这个状态
terminated 当线程池彻底停止

线程池的创建方式

通过Executors工厂方法创建
Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

通过new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)自定义创建

线程池的七大参数

corePoolSize 线程池核心线程大小
maximumPoolSize 线程池最大线程数量
keepAliveTime 空闲线程存活时间
unit 空闲线程存活时间单位
workQueue 工作队列
threadFactory 线程工厂
handler 拒绝策略

线程池的拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
【默认】ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务 ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
可以通过ThreadPoolExecutor重载的构造方法进行设置

【注】Excutors在创建线程池时没有限制任务队列的长度,容易导致OOM异常 OOM内存溢出

线程池的执行流程

image.png

Executor框架

我们知道线程池就是线程的集合,线程池集中管理线程,以实现线程的重用,降低资源消耗,提高响应速度等。线程用于执行异步任务,单个的线程既是工作单元也是执行机制,从JDK1.5开始,为了把工作单元与执行机制分离开,Executor框架诞生了,他是一个用于统一创建与运行的接口。Executor框架实现的就是线程池的功能。

线程安全

当多个线程同时操作一个共享资源的时候,可能会发生数据污染的情况

锁的四种状态

锁有四种状态:无锁状态、偏向锁、轻量级锁、重量级锁
随着锁的竞争,锁的状态会从偏向锁到轻量级锁,再到重量级锁。而且锁的状态只有升级,没有降级。
也就是只有偏向锁->轻量级锁->重量级锁,没有重量级锁->轻量级锁->偏向锁。
锁状态的改变是根据竞争激烈程度进行的,在几乎无竞争的条件下,会使用偏向锁,在轻度竞争的条件下,会由偏向锁升级为轻量级锁, 在重度竞争的情况下,会升级到重量级锁。

常见的锁机制

【锁的分类】 悲观锁,乐观锁,独占锁,共享锁,公平锁,非公平锁,分布式锁,自旋锁

【乐观锁与悲观锁】
mysql吧,悲观锁,主要是表锁,行锁还有间隙锁,叶锁,读锁,因为这些锁在被触发的时候势必引起线程阻塞,所以叫悲观。另外乐观锁其实在mysql本身中不存在的,但是mysql提供了种mvcc的机制,支持乐观锁机制
mvcc:
只是在innodb引擎下存在,mvcc是为了满足事务的隔离,通过版本号的方式,避免同一数据不同事务间的竞争,所说的乐观锁只在事务级别为读未提交读提交,才会生效,
多版本并发控制,保证数据操作在多线程过程中,保证事务隔离的机制,可以降低锁竞争的压力,保证比较高并发量,这个过程。在每开启一个事务时,会生成一个事务的版本号,被操作的数据会生成一条新的数据行(临时),但是在提交前对其他事务是不可见的,对于数据的更新操作成功,会将这个版本号更新到数据的行中,事务提交成功,将新的版本号,更新到此数据行(永久)中,这样保证了每个事务操作的数据,都是相互不影响的,也不存在锁的问题。

【独占锁与共享锁】
独占锁很明显就是持锁的线程只能有一个,共享锁则可以有多个
共享锁是为了提高程序的效率,举个例子数据的操作有读写之分,对于写的操作加锁,保证数据正确性,而对于读的操作如果不加锁,在写读操作同时进行时,读的数据有可能不是最新数据,如果对读操作加独占锁,面对读多写少的程序肯定效率很低,所有就出现了共享锁,对于读的的操作就使用共享的概念,但是对于写的操作则是互斥的,保证了读写的数据操作都一致,在java中上述的锁叫读写锁

Volatile 与synchronized

【volatile】
Volatile修饰的数据,在被某个线程修改后,会被及时的回写到主内存,然后其他线程再获取时,就是新的数据,听起来很美好,但是Volatile没有办法控制线程的顺序,当一个数据(新数据)即将被修改到主内存时,刚好,另外一个线程从主内存读了数据(老数据),并又进行了一波操作,又将数据(更新的数据)回写到了主内存,整个过程(新数据)完全没有起到一毛钱作用,最终导致了数据的错误。
具备可见性、有序性、但不具备原子性。

【synchronized关键字】
synchronized关键字,是语言自带的,也叫内置锁,synchronized关键字,我们都知道被synchronized修饰的方法或者代码块,在同一时间内,只允许一个线程执行,是明显的独享锁,synchronized的实现机制?可以参考AQS的实现方式,只是AQS使用显示的用lock.lock调用,而sync作为关键字修饰,你可以认为在synchronized修饰的地方,自动添加了lock方法,结束的地方进行了unlock释放锁的方法,只是被隐藏了,我们看不到。

它本身实现有两部分:monitor对象,线程,工作机制还是线程抢占对象使用权,对象都有自己的对象头,存储了对象的很多信息,其中有一个是标识被哪个线程持有,对比AQS,线程从修改stat,变为修改monitor的对象头,线程的等待区域动 AQS中的队列,变为monitor对象中的某个区域。

避免死锁

说到怎么避免死锁的话,就要聊一聊产生死锁的四个条件,一个是互斥条件:就是一个资源每次,只能被一个进程 使用。还一个是请求与保持条件:就是一个进程因为请求资源而阻塞的时候嘛,对已获得的资源就是保持不放还一 个是不剥夺条件:就是进程已获得的资源,在他没有使用完之前,不能强行剥夺还一个是循环等待条件:就是说若干 进程之间,就是形成一种头尾相接的循环等待资源关系。这就是产生死锁的四个必要条件嘛,只要破坏了一个就可 以避免死锁的发生

设计模式

面向对象6大原则

文章分类技术进阶阅读数3999
1.单一原则。一个类应该有且只有一个变化的原因。单一职责原则将不同的职责分离到单独的类,每一个职责都是一个变化的中心。需求变化时,将通过更改职责相关的类来体现。如果一个类拥有多于一个的职责,则多个职责耦合在一起,会有多于一个原因来导致这个类发生变化。一个职责的变化可能会影响到其他的职责,另外,把多个职责耦合在一起,影响复用性。

2.里氏替换原则,就是要求继承是严格的is-a关系。所有引用基类的地方必须能透明地使用其子类的对象。在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。

3.依赖倒置原则。依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。低层模块尽量都要有抽象类或接口,或者两者都有。变量的声明类型尽量是抽象类或接口。

4.接口分离原则。一个类对另一个类的依赖应该建立在最小的接口上,通俗的讲就是需要什么就提供什么,不需要的就不要提供。接口中的方法应该尽量少,不要使接口过于臃肿,不要有很多不相关的逻辑方法。

5.多用组合(has-a),少用继承(is-a)。如果新对象的某些功能在别的已经创建好的对象里面已经实现,那么应当尽量使用别的对象提供的功能,使之成为新对象的一部分,而不要再重新创建。可以降低类与类之间的耦合程度。

6.开闭原则。对修改关闭,对扩展开放。在软件的生命周期内,因为变化,升级和维护等原因需要对软件原有代码进行修改,可能会给旧代码引入错误,也有可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。不过这要求,我们要对需求的变更有前瞻性和预见性。其实只要遵循前面5中设计模式,设计出来的软件就是符合开闭原则的

单例模式

饿汉式 在类加载时创建对象
懒汉式 在第一次调用时创建对象

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。

工厂模式

【1】工厂模式
工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
【2】工厂模式核心
意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
主要解决:主要解决接口选择的问题。
何时使用:我们明确地计划不同条件下创建不同实例时。
如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。
优点:
1、一个调用者想创建一个对象,只要知道其名称就可以了。
2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
3、屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了【系统的复杂度】,同时也增加了系统具体类的依赖。这并不是什么好事。
注意事项:作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。

代理模式

【实现方式】
基于JDK实现的代理模式 实现
基于cglib实现的 继承
【概念】
在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。
在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。
【优点】:
1、职责清晰。
2、高扩展性。
3、智能化。
【缺点】:
1、由于在客户端和真实主题之间增加了代理对象,有些类型的代理模式可能会造成请求的处理速度变慢。
2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
【使用场景】:按职责来划分,通常有以下使用场景:
1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。

职责链模式 servlet中的filter

【概念】
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。
在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
【意图】:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
【主要解决】:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
【何时使用】:在处理消息的时候以过滤很多道。
【如何解决】:拦截的类都实现统一接口。
【关键代码】:Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。
【应用实例】: 1、红楼梦中的”击鼓传花”。 2、JS 中的事件冒泡。 3、JAVA WEB 中 Apache Tomcat 对 Encoding 的处理,Struts2 的拦截器,jsp servlet 的 Filter。
【优点】: 1、降低耦合度。它将请求的发送者和接收者解耦。 2、简化了对象。使得对象不需要知道链的结构。 3、增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。 4、增加新的请求处理类很方便。
【缺点】: 1、不能保证请求一定被接收。 2、系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。 3、可能不容易观察运行时的特征,有碍于除错。
【使用场景】: 1、有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。 2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。 3、可动态指定一组对象处理请求。
【sevlet中的职责链模式】
当创建了多个Filter之后,客户端传来一个Request请求,它就面对着这一个Filter链,职责琏模式就体现在这里。这个请求会在这个 Filter链上一个一个被传递下去对它进行预处理,处理完成之后就传给下一个Filter直到最后一个,然后才交给web进行相应的访问和处理。它

MVC模式 springMVC

MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。
Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
View(视图) - 视图代表模型包含的数据的可视化。
Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。

原型模式 java中的clone方法

【概念】原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
【意图】:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
【主要解决】:在运行期建立和删除原型。
【何时使用】: 1、当一个系统应该独立于它的产品创建,构成和表示时。 2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。 3、为了避免创建一个与产品类层次平行的工厂类层次时。 4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
【如何解决】:利用已有的一个原型对象,快速地生成和原型对象一样的实例。
【关键代码】: 1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。 2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些”易变类”拥有稳定的接口。
【应用实例】: 1、细胞分裂。 2、JAVA 中的 Object clone() 方法。
【优点】: 1、性能提高。 2、逃避构造函数的约束。
【缺点】: 1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。 2、必须实现 Cloneable 接口。
【使用场景】: 1、资源优化场景。 2、类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。 3、性能和安全要求的场景。 4、通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。 5、一个对象多个修改者的场景。 6、一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。 7、在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。
【注意事项】:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。

装饰器模式

【概念】装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
【意图】:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
【主要解决】:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。
【何时使用】:在不想增加很多子类的情况下扩展类。
【如何解决】:将具体功能职责划分,同时继承装饰者模式。
【关键代码】: 1、Component 类充当抽象角色,不应该具体实现。 2、修饰类引用和继承 Component 类,具体扩展类重写父类方法。
【应用实例】: 1、孙悟空有 72 变,当他变成”庙宇”后,他的根本还是一只猴子,但是他又有了庙宇的功能。 2、不论一幅画有没有画框都可以挂在墙上,但是通常都是有画框的,并且实际上是画框被挂在墙上。在挂在墙上之前,画可以被蒙上玻璃,装到框子里;这时画、玻璃和画框形成了一个物体。
【优点】:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
【缺点】:多层装饰比较复杂。
【使用场景】: 1、扩展一个类的功能。 2、动态增加功能,动态撤销。
【注意事项】:可代替继承

JVM

四种引用

在 JDK1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软 引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

【强引用】
强引用是最传统的 “引用” 的定义 ,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。

【软引用】
软引用是用来描述一些还有用,但非必要的对象。只被软引用关联着的对象,在系统将要发生内存溢出钱,会把这些对象列进回收范围之中进行二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后提供了 SoftReference 类实现软引用。

【弱引用】
弱引用也是用来描述哪些非必需对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收之被弱引用的关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。

【虚引用】
虚引用也称为 “幽灵引用” 或者 “幻影引用” ,它是最弱的一种引用关系。一个对象是否有需引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来去的一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供 了PhantomReference类来实现虚引用。

jvm类的加载过程

装载:程序运行之前jvm会把编译完成的.class二进制文件加载到内存,供程序使用。
验证:确保类加载的正确性。
准备:为类的静态变量分配内存,将其初始化为默认值 。
解析:类中的符号引用转化为直接引用。
初始化:为类的静态变量赋予正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序编写者为变量分配的真正的初始值。
使用:
卸载:

jvm 双亲委派模型

【双亲委托模型】是约定类加载器的加载机制,当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载。
双亲委托模型的【工作过程】是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
使用双亲委托机制的【好处】是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委托给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。相反,如果没有使用双亲委托模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
双亲委托模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委托的代码都集中在java.lang.ClassLoader的loadClass()方法中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载。

jvm内存模型

程序计数器(线程私有)
本地方法栈(线程私有)
java虚拟机栈(线程私有)
堆(线程共有)
方法区(线程共有)
元空间(存在于内存区) jdk1.8之后 代替方法区 位置存在于本地内存区

内存溢出和栈溢出

栈溢出
若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。
OutOfMemoryError(内存溢出)
当可动态扩展的虚拟机栈在扩展时无法申请到足够的内存,就会抛出该异常。
原生内存不足(操作系统不允许申请更大的内存)
永久代或元空间不足
JVM执行GC耗时太久

jvm算法

标记清除算法
复制算法
标记整理算法
分代收集算法 新生代—>标记清除 老年代—>标记整理算法

GC回收机制 触发

GC的触发使用的是可达性分析法 和 引用计数法
引用计数法:系统为对象添加计数器,当有新的引用的时候+1,引用失效减1,这种方式无法解决两个对象循环依赖的问题
可达性分析算法:通过对象的引用链来判断该对象是否需要被回收,通过GC Roots的对象作为起始点,从这个起始点来向下搜索,搜索所走过的路径被称为引用链,当一个GC Roots没有任何引用链,就代表这个对象是不可用的,可以回收

类加载器

从JVM的角度,可将类加载器分为两种:
1.启动类加载器
由C++语言实现,是虚拟机自身的一部分
负责加载存放在<JAVA_HOME>\lib目录中、或被-Xbootclasspath参数所指定路径中的、且可被虚拟机识别的类库,无法被Java程序直接引用,如果自定义类加载器想要把加载请求委派给引导类加载器的话可直接用null代替
2.其他类加载器
由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader,可被Java程序直接引用。常见几种:
1.扩展类加载器
A.由sun.misc.Launcher$ExtClassLoader实现B.负责加载<JAVA_HOME>\lib\ext目录中的、或者被java.ext.dirs系统变量所指定的路径中的所有类库
2.应用程序类加载器
A.是默认的类加载器,是ClassLoader#getSystemClassLoader()的返回值,故又称为系统类加载器B.由sun.misc.Launcher$App-ClassLoader实现C.负责加载用户类路径上所指定的类库
3.自定义类加载器
如果以上类加载起不能满足需求,可自定义,需要注意的是:虽然数组类不通过类加载器创建而是由JVM直接创建的,但仍与类加载器有密切关系,因为数组类的元素类型最终还要靠类加载器去创建

GC回收器

1.串行Serial Collector 单线程回收

它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。

从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了标记 - 整理(Mark-Compact)算法。清空整个Heap中的垃圾对象,清除元数据去已经被卸载的类信息,并进行压缩。

新生代的复制算法。Serial GC 的对应 JVM 参数是:-XX:+UseSerialGC

2.并行回收器(Paraller Collector )

又称throughput collector 。是jvm默认的回收器。Serial的升级版本,多线程进行GC。常见应用场景是配合老年代CMS GC工作参数如下

3.并发标记扫描收回器(CMS Collector)

管理新生代方式和Paralel和Serial GC相同。而是在老年代中并发处理。尽量减少停顿时间。

CMS收集器(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器,是基于“标记-清除”算法。

CMS的整个过程有4个步骤:

初始标记——并发标记——重新标记——并发清除

初始标记:CMS initial mark仅仅是标记一下GC Roots能直接关联的对象,速度快;需要stop the world

并发标记:CMS concurrent mark是进行GC Roots Tracing的过程;

重新标记:CMS remark是修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,停顿时间比初始标记长,比并发标记短;需要stop the world

并发清除:CMS concurrent sweep,清除算法会在收集结束时产生大量空间碎片,有可能导致没有足够大的连续空间来分配当前对象而触发一次Full GC。

缺点:1)CMS收集器对CPU资源非常敏感; 2)CMS收集器无法处理浮动垃圾,可能出现”Concurrent Mode Failure”失败(备选用Serial Old)而导致另一次Full GC的产生; 3)CMS是一款基于“标记-清除”算法的收集器,在收集结束后会产生大量空间碎片。

优点:并发收集;低停顿(并发低停顿收集器)

XX:+USeParNewGC 打开并发标记扫描垃圾回收器。

4.G1 垃圾回收器G1 (Garbage first )

是JDK7的新特性。jdk7以上都可以自主设置JVM GC类型。G1会将堆内存划分成相互独立的区块(默认1024),每一块都可能是不连续的 O(old区),Y(young区)区块(相对于CMS中O,Y区块是连续的)。G1会第一时间处理垃圾最多的区块。这个是garbage First的原因之一。

优点:

并发与并行、分代收集、空间整合、可预测的停顿

G1收集器运作的步骤:

初始标记——并发标记——最终标记——筛选回收

  • 初始标记:initial marking,标记一下GC Roots能直接关联的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,需要停顿线程,耗时短;
    - 并发标记:concurrent marking,从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时长,但可与用户程序并发执行;
    - 最终标记:final marking,修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,对象变化记录存在线程Remember Set Logs中,然后把这些数据合并到Remember Set中,该阶段停顿线程,但是可并行执行;
    - 筛选回收:live data counting and evacuation,对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。
    OOM
    【出现可能性】:
    原生内存不足
    永生代或元空间内存不足
    jvmGC耗时太久

集合

ArrayList
  1. ArrayList集合的底层是由一个可变的动态 Object[]数组组成的,由于他是由数组构成的,所以该集合类框架对于随机访问的效率很高。<br />在 ArrayList中,其扩容的方法主要为 grow()方法:<br /> 其实在ArrayList底层数组也是不可以改变的,底层动态数组的实现逻辑是通过重新创建新数组的方式来实现的。也就是说它底层处理的逻辑是当ArrayList发现底层数组的大小已经超过了数组默认初始化大小时,就会创建一个新数组,然后把原数组中的数据拷贝到新数组中,然后操作这个新数组使用。如果当发现新创建的数组大小还是不够我们存储时,继续重复上面的逻辑。所以我们在使用ArrayList集合类时,是不用考虑底层数组的大小的。<br /> JDK1.7:初始容量10,扩容1.5倍<br /> JDK1.8:初始容量0,延迟数组的创建,节省内存

linkedList

HashSet和TreeSet的区别?
  1. HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的, 只不过Set用的只是Map的key。(注意理解一下这句话,可以参考 HashSet与HashMap的区别)

  2. Map的key和Set都有一个共同的特性就是集合的唯一性.TreeMap更是多了一个有序性.

  3. hashCode和equal()是HashMap用的, 因为无需排序所以只需要关注定位和唯一性即可.
    a. hashCode是用来计算hash值的,hash值是用来确定hash表索引的.
    b. hash表中的一个索引处存放的是一张链表, 所以还要通过equal方法循环比较链上的每一个对象 才可以真正定位到键值对应的Entry.
    c. put时,如果hash表中没定位到,就在链表前加一个Entry,如果定位到了,则更换Entry中的value,并返回旧value
    d. 覆写key的hashCode()和equal()时一定要注意,不要把它们和可变属性关联上,否则属性变了之后hashCode会变,equal也会为false, 这样在Map中就找不不到它了,而且这样的对象因为找不到它所以得不到释放,这样就变成了一个无效引用了(相当于内存泄漏).

  4. 由于TreeMap需要排序,所以需要一个Comparator为键值进行大小比较.当然也是用Comparator定位的.
    a. Comparator可以在创建TreeMap时指定,这时排序时使用Comparator.compare
    b. 如果创建时没有指定Comparator,那么就会使用key.compareTo()方法,这就要求key必须实现Comparable接口.
    c. TreeMap是使用Tree数据结构实现的,所以使用compare接口就可以完成定位了

    HashMap

    DK1.7:数组(也有叫做“位桶”的)+链表
    JDK1.7用的是头插法
    JDK1.8:数组+链表/红黑树
    JDK1.8及之后使用的都是尾插法
    初始size为16
    JDK1.7是用单链表进行的纵向延伸,当采用头插法就是能够提高插入的效率,但是也会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

扩容后数据存储位置的计算方式也不一样:
1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
2、而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
在计算hash值的时候,JDK1.7用了9次扰动处理=4次位运算+5次异或,而JDK1.8只用了2次扰动处理=1次位运算+1次异或。
JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(nlogN)提高了效率)

ConcurrentHashMap源码分析

1)ConcurrentHashMap是线程安全的并发容器,是用来替代在多线程环境下的 HashMap,因为 HashMap 是线程不安全的,多线程环境下 put 操作可 能会导致【死循环】.

2)ConcurrentHashMap1.7 是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。

3)ConcurrentHashMap 采用了【分段锁技术】,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要 做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。 在进行 put 操作时首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以put 操作时仍然需要加锁 处理。 首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存 在竞争,则利用 scanAndLockForPut() 自旋获取锁,如果达到一定的次数还 没有获得,则改为阻塞获得锁,保证一定能成功。成功后
1. 将当前 Segment 中的 table 通 key 的 hashcode 定位到 HashEntry。
2. 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
3. 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判 断是否需要扩容。
4. 最后会解除在 1 中所获取当前 Segment 的锁。
Get:在进行 get 方法时只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取 时都是最新值。ConcurrentHashMap 的 get 方法是非常高效的,因为整个 过程都不需要加锁。
在 JDK1.8 中抛弃了原有的 Segment 分段锁,而采用了 CAS+synchronized 来 保证并发安全性。解决了在 1.7 中的 查询遍历链表效率太低的问题,并也将 1.7 中存放数据的 HashEntry 改为 Node。
 Put 方法时:
1、 根据 key 计算出 hashcode 找出对应位置 f。
2、判断是不是第一次添加,如果是第一次,初始化。
3、f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数 据,利用 CAS 尝试写入,失败则自旋保证成功。
4、如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
5、如果都不满足,则利用 synchronized 锁写入数据。
6、如果数量大于 8 时则要转换为红黑树
Get 方法:
根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
如果是红黑树那就按照树的方式获取值。
就不满足那就按照链表的方式遍历获取值。

hashTable的与hashMap的区别

1、HashTable 线程安全,HashMap 非线程安全 2、Hashtable 不允许 null 值(key 和 value 都不可以),HashMap 允许 null 值(key 和 value 都可以)。 3、两者的遍历方式大同小异,Hashtable 仅仅比 HashMap 多一个 elements 方法。

mybatis

怎样生成mapper接口的实现类执行sql

mybatis底层封装了JDBC,使用JDBC实现对数据库的连接与操作
mybatis底层使用jdk动态代理为mapper接口生成代理proxy对象,代理对象proxy会拦截接口的方法,执行sql语句

mybatis源码分析

• Mybatis配置文件
• SqlMapConfig.xml,此文件作为mybatis的全局配置文件,配置了mybatis的运行环境等信息。
• Mapper.xml,此文件作为mybatis的sql映射文件,文件中配置了操作数据库的sql语句。此文件需要在 SqlMapConfig.xml中加载。
• SqlSessionFactory
• 通过mybatis环境等配置信息构造SqlSessionFactory,即会话工厂。
• SqlSession
• 通过会话工厂创建sqlSession即会话,程序员通过sqlsession会话接口对数据库进行增删改查操作。
• Executor执行器
• mybatis底层自定义了Executor执行器接口来具体操作数据库,Executor接口有两个实现,一个是基本执行器(默认)、一个是缓存执行器,sqlsession底层是通过executor接口操作数据库的。
• MappedStatement
• 它也是mybatis一个底层封装对象,它包装了mybatis配置信息及sql映射信息等。mapper.xml文件中一个select\insert\update\delete标签对应一个Mapped Statement对象,select\insert\update\delete标签的id即是Mapped statement的id。
• Mapped Statement对sql执行输入参数进行定义,包括HashMap、基本类型、pojo,Executor通过MappedStatement在执行sql前将输入的java对象映射至sql中,输入参数映射就是jdbc编程中对preparedStatement设置参数。
• Mapped Statement对sql执行输出结果进行定义,包括HashMap、基本类型、pojo,Executor通过MappedStatement在执行sql后将输出结果映射至java对象中,输出结果映射过程相当于jdbc编程中对结果的解析处理过程。

spring

spring bean的生命周期

实例化(Instantiation)
属性赋值(Populate)
初始化(Initialization)
销毁(Destruction)

spring bean的作用域

singleton 在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,bean作用域范围的默认值。
prototype 每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean()。
request 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于web的Spring WebApplicationContext环境。
session 同一个HTTP Session共享一个Bean,不同Session使用不同的Bean。该作用域仅适用于web的Spring WebApplicationContext环境。
application 限定一个Bean的作用域为ServletContext的生命周期。该作用域仅适用于web的Spring WebApplicationContext环境。

spring循环依赖产生和解决

Spring 的三种循环依赖

  • 构造器的循环依赖,处理不了,直接抛出 BeanCurrentlylnCreationException 异常;
  • 非单例的循环依赖,处理不了;
  • 单例模式的 setter 或 @Autowired 循环依赖,通过三级缓存来处理。

    一级、二级、三级缓存介绍

    singletonObjects,一级缓存。 用于存放已实例化和初始化完成(属性赋值)的 Bean,即我们常用到 Bean;
    earlySingletonObjects,二级缓存。 存放已实例化但未初始化的 Bean 引用;
    singletonFactories,三级缓存。 存放已实例化,但未初始化的 Bean 工厂。
    二级缓存和三级缓存的区别在于,二级缓存的 Bean 会增加一些扩展功能。比如二级缓存的 Bean 引用为动态代理, 先从三级缓存中获取 Bean,然后为其创建代理对象,之后放到二级缓存中,这也是为什么需要二级缓存的原因。
      以 B 对象,A 属性为例,使用三级缓存 B 对象获取到的是半成品 A 对象(属性)的原始引用。如果在最后创建完成,放入一级缓存中的完成品 A 是代理引用,这与期望要求的不同,所以会多一个二级缓存,用于存放三级缓存的 Bean 的代理引用。

Bean循环依赖时,创建流程如下

  1. 先创建BeanA,先实例化BeanA并包装为BeanFactory并放入三级缓存中.
  2. 给BeanA进行属性填充时检查依赖,发现BeanB未加载过,则先去加载BeanB
  3. BeanB创建过程首先也要包装成BeanFactory放到三级缓存,填充属性时则是从三级缓存获取Bean将BeanA填充进去
  4. BeanB填充BeanA从三级缓存中的BeanAFacotry获取BeanA
  5. 获取主要通过ObjectFactory.getObject方法,该方法调用getEarlyBeanReference方法,他会创建Bean/Bean的代理并删除BeanA的三级缓存,加入二级缓存
  6. BeanB初始化完毕加入一级缓存,BeanA继续执行初始化,初始化完毕比较BeanA二级缓存和一级缓存是否一致,一致则加入一级缓存删除二级缓存

三级缓存的解决思路

还是以上面的 A 依赖 B,B 依赖 A 为例。

Spring 发现 A 对象,实例化 A,但没有放到 Spring 对象池(一级缓存中),将半成品 A 放到三级缓存中;
A 对象还需要完成初始化,注入属性 B,于是从 Spring 对象池中获取 B 实例;
但没获取到,于是 Spring 会进入到 B 属性,同样是实例化 B,但没放到 Spring 对象池中,将半成品 B 放到三级缓存中;
接着 B 对象获取属性 A(对象),从三级缓存中获取到半成品 A,进行属性注入,至此 B 对象完成实例化和属性初始化,会放到一级缓存中,并返回;
A 对象获取到 B 对象,进行属性注入,于是 A 对象也完成实例化和初始化会放到一级缓存中。
在 B 进行属性注入,获取 A 对象时,会从三级缓存中先获取一个半成品 A,只有实例化,还没完成属性初始化的 A。这就打破了循环,解决循环依赖。

如何实现一个ioc容器

image.png

对IOC的理解

image.png

image.png

AOP的理解

image.png
1、动态代理概述代理类Proxy和RealSubject应该实现了相同的功能接口,在面向对象的编程之中,如果想要两个对象实现相同的功能,有以下两种方式:

1) 定义一个功能接口,然后代理类Proxy和真实类RealSubject都实现这个接口。
2) 代理类Proxy继承RealSubject,这样Proxy则拥有了RealSubject的功能,Proxy还可以通过重写RealSubject中的方法,来实现多态。
其中JDK中提供的创建动态代理的机制就是以1方式设计的,而cglib则是以2方式设计的。

2、JDK的动态代理机制
Jdk为RealSubject对象创建动态代理对象,主要做了以下工作:

1) 获取RealSubject上的所有接口列表。
2) 确定要生成的动态代理类的类名,默认为com.sun.proxy.$ProxyXXX
3) 根据需要实现接口信息,在代码中动态创建该Proxy的字节码
4) 将对应的字节码转换为对应的class对象
5) 创建InvocationHandler实例,用来处理Proxy所有方法调用
6) Proxy的class对象以创建的handler对象为参,实例化Proxy对象
Jdk通过java.lang.reflect.Proxy来支持动态代理,一般情况下,使用方法newProxyInstanceof来创建Proxy类,而对于InvocationHandler,需要实现它的invoke方法,在调用代理对象中的每一个方法时,在代码内部,都是直接调用了InvocationHandler的invoke方法,而invoke方法根据代理类传递给自己的method参数来区分是什么方法。

3、cglib的动态代理机制
JDK中提供的生成动态代理类的机制有个鲜明的特点是: 某个类必须有实现的接口,而生成的代理类也只能代理某个类接口定义的方法,果某个类没有实现接口,那么这个类就不能同JDK产生动态代理了!CGLIB(Code Generation Library),是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。

cglib 创建某动态代理类的模式是:

1) 查找类上的所有非final的public类型的方法定义
2) 将这些方法的定义转换成字节码
3) 将组成的字节码转换成相应的代理的class对象
4) 实现MethodInterceptor接口,用来处理对代理类上所有方法的请求(和InvocationHandler的功能和角色是一样的)cglib需要的jar包有asm.jar、cglib.jar

aop通知分为如下几种:

前置通知(before):在执行业务代码前做些操作,比如获取连接对象
后置通知(after):在执行业务代码后做些操作,无论是否发生异常,它都会执行,比如关闭连接对象
异常通知(afterThrowing):在执行业务代码后出现异常,需要做的操作,比如回滚事务
返回通知(afterReturning):在执行业务代码后无异常,会执行的操作
环绕通知(around):,这个目前跟我们谈论的事务没有对应的操作,所以暂时不谈

spring 事务的传播行为

技术栈 - 图8

spring mvc

spring MVC运行流程

image.png
1、用户发送请求至前端控制器 DispatcherServlet
2、DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器。
3、处理器映射器根据请求 url 找到具体的处理器,生成处理器对象及处理器拦截器(如果有
则生成)一并返回给 DispatcherServlet。
4、DispatcherServlet 通过 HandlerAdapter 处理器适配器调用处理器
5、执行处理器(Controller,也叫后端控制器)。
6、Controller 执行完成返回 ModelAndView
7、HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet

spring MVC中使用的设计模式

SpringBoot

自动装配原理
  1. 当创建一个springboot项目时,在springboot的启动类上存在一个@SpringBoot注解,springboot项目通过调用SpringApplcationrun的方法来启动程序,使用默认配置的指定资源启动SpringAplication并生成一个ApplocationContext对象<br /> @SpringBootApplication中,包含三个核心注解,分别是<br /> @SpringBootConfiguration 声明被@Configuration注解修饰的类为IOC容器,支持javaConfig方式启动<br /> @ComponentScan 扫描当前目录下的所有包,以及子包下的注解<br /> @EnableAutoConfiguration 开启自动装配,内部包含@improt注解和@AutoConfigurationPackage注解 通过这两个注解找到META-INF/spring.factories文件中文件中的keyEnableAutoConfiguration的对应值,找到相对应的配置类,执行相应的一些配置,对其进行加载,完成自动装配

Redis

4.1 Redis的数据类型

常用的5种数据结构:

  • key-string:一个key对应一个值。
    - key-hash:一个key对应一个Map。
    - key-list:一个key对应一个List列表。
    - key-set:一个key对应一个Set集合。
    - key-zset:一个key对应一个有序的Set集合。等价于:Map

另外3种数据结构:

  • HyperLogLog:计算近似值的。
    - GEO:地理位置。
    - BIT:一般存储的也是一个字符串,存储的是一个byte[]。
    Redis持久层
    Redis提供2种方式实现持久化:1.RDB 2.AOF
    同时开启RDB和AOF的注意事项:
    如果同时开启了AOF和RDB持久化,那么在Redis宕机重启之后,需要加载一个持久化文件,优先选择AOF文件。
    如果先开启了RDB,再次开启AOF,如果RDB执行了持久化,那么RDB文件中的内容会被AOF覆盖掉。

【RDB】
RDB是Redis默认的持久化机制
RDB持久化文件,速度比较快,而且存储的是一个二进制的文件,传输起来很方便。
RDB持久化的时机:
save 900 1:在900秒内,有1个key改变了,就执行RDB持久化。
save 300 10:在300秒内,有10个key改变了,就执行RDB持久化。
save 60 10000:在60秒内,有10000个key改变了,就执行RDB持久化。
RDB无法保证数据的绝对安全。

【AOF】
AOF持久化机制默认是关闭的,Redis官方推荐同时开启RDB和AOF持久化,更安全,避免数据丢失。
AOF持久化的速度,相对RDB较慢的,存储的是一个文本文件,到了后期文件会比较大,传输困难。
AOF持久化时机。
appendfsync always:每执行一个写操作,立即持久化到AOF文件中,性能比较低。
appendfsync everysec:每秒执行一次持久化。
appendfsync no:会根据你的操作系统不同,环境的不同,在一定时间内执行一次持久化。
AOF相对RDB更安全,推荐同时开启AOF和RDB。

4.3 Redis的集群策略

1.主从复制
单机版 Redis存在读写瓶颈的问题
技术栈 - 图10
2.哨兵机制
哨兵可以帮助我们解决主从架构中的单点故障问题
3.集群多主多从
Redis-Cluster
Redis集群在保证主从加哨兵的基本功能之外,还能够提升Redis存储数据的能力
技术栈 - 图11

4.4 Redis的过期策略
  • 定期删除:Redis每隔一段时间就去会去查看Redis设置了过期时间的key,会再100ms的间隔中默认查看3个key。

- 惰性删除:如果当你去查询一个已经过了生存时间的key时,Redis会先查看当前key的生存时间,是否已经到了,直接删除当前key,并且给用户返回一个空值。

4.5 Redis的淘汰机制

在Redis内存已经满的时候,添加了一个新的数据,执行淘汰机制。
volatile-lru:在内存不足时,Redis会再设置过了生存时间的key中干掉一个最近最少使用的key。
allkeys-lru:在内存不足时,Redis会再全部的key中干掉一个最近最少使用的key。
volatile-lfu:在内存不足时,Redis会再设置过了生存时间的key中干掉一个最近最少频次使用的key。
allkeys-lfu:在内存不足时,Redis会再全部的key中干掉一个最近最少频次使用的key。
volatile-random:在内存不足时,Redis会再设置过了生存时间的key中随机干掉一个。
allkeys-random:在内存不足时,Redis会再全部的key中随机干掉一个。
volatile-ttl:在内存不足时,Redis会再设置过了生存时间的key中干掉一个剩余生存时间最少的key。
noeviction:(默认)在内存不足时,直接报错。
指定淘汰机制的方式:maxmemory-policy 具体策略,设置Redis的最大内存:maxmemory 字节大小

4.7redis穿透

技术栈 - 图12
现象:
如果每次都去查一个“缓存和数据库中都必不存在的数据(如id=-1的数据)”,因为缓存中不存在,那么每次请求都会打到DB上,从而导致缓存失去意义,在高并发的情况下就可能导致数据库崩溃,这就是缓存穿透。
解决方案:
1、规范key过滤
规范key的命名,并且统一缓存查询的入口,在入口处对key的命名格式进行检测,过滤掉不规范key的访问,这样可以过滤掉大部分的恶意攻击。如约定项目中Redis缓存key的前缀都是以”公司名项目名_REDIS“开头,不符合这个约定的key在一开始就过滤掉。
2、缓存空值
简单粗暴,如果查询DB返回的数据为空,我们仍然把这个空值放到Redis缓存中,只是将它的过期时间设置的很短,另外为了避免不必要的内存消耗,可以定期清理空值的key。
3、加锁
根据key从缓存中获取到的value为空时,先锁上,再去查DB将数据加载到缓存,若其它线程获取锁失败,则等待一段时间后重试,从而避免了大量请求直接打到DB。单机可以使用synchronized或ReentrantLock加锁,分布式环境需要加分布式锁,如Redis分布式锁。
4、布隆过滤器
我们想这样一个问题,如果想判断某个元素是不是在一个集合里,一般做法是将集合中所有的元素保存起来,然后通过比较确定,比如HashMap。但是随着集合中元素的增加,数据量超大时,我们需要的存储空间也越来越大,甚至超过服务器内存,这时我们就不能再用HashMap等数据结构了。
这时布隆过滤器就出场了,它的空间效率非常好,它是一个二进制向量,每一位存放的是0或1,初始时默认为0,长下面这样:
技术栈 - 图13
当一个元素加入集合时,通过 K 个 Hash 函数将这个元素映射成 k 个值 :K1、K2、K3…,把向量中下标为K1、K2、K3…的位置置为1 。
比如,元素X进来,将X作为参数,通过3个hash函数的计算,分别得到3个值:Hash1(X)=5;Hash2(X)=2;Hash3(X)=9;那么我们就将布隆过滤器中下标为5、2、9的位置分别置为1,如下:
技术栈 - 图14
可以看出,布隆过滤器根本没有存放完整的数据,只是运用一系列随机映射函数计算出位置,然后填充二进制向量,所以它的空间效率非常好。
预先将所有缓存数据的key存放到布隆过滤器中,当一个查询请求过来的时候,先判断这个key在布隆过滤器中是否存在?
> 如果不存在,直接返回提示,都不用去查缓存更不用说DB了;
> 如果存在,则去查缓存,但我们知道布隆过滤器判断存在有一定的误判率,这里我是这样理解的,如果这个误判率针对你们的业务场景是可被接受的则可以忽略,另外我们在用Guava实现布隆过滤器的时候可以指定误判率不超过多少,你可以指定一个可被你接受的值。再或者,因为布隆过滤器可以过滤掉绝大多数的恶意key,针对少部分的漏网之鱼,我们可以在缓存层面使用功能上面说过的缓存空值或加锁的方案。

4.7 Redis击穿

技术栈 - 图15
现象:
缓存击穿和缓存穿透不一样!说缓存击穿之前,我们先来了解一个概念——热点key,某个访问非常频繁,访问量非常大的一个缓存key,我们叫做热点key。
缓存击穿是指某个热点key在失效的瞬间(一般是缓存时间到期),持续的大并发请求穿破缓存,直接打到数据库,就像在一个屏障上凿开了一个洞,造成数据库压力瞬间增大,这就是缓存击穿。
解决方案:
1、设置热点key永不过期
2、加锁,根据热点key从缓存中获取到的value为空时,先锁上,再去查DB将数据加载到缓存,若其它线程获取锁失败,则等待一段时间后重试,从而避免了大量请求直接打到DB。单机可以使用synchronized或ReentrantLock,分布式需要加分布式锁,如Redis分布式锁。【为了不阻塞对其他key的请求,此处可以用热点key来加锁】

4.8 Redis雪崩

技术栈 - 图16
现象:
缓存雪崩是指缓存由于某些原因整体或者大量失效,导致大量请求打到后端数据库,从而导致数据库崩溃,整个系统崩溃,发生灾难。导致缓存整体或大量失效的场景一般有:
1、缓存服务宕机,如Redis集群彻底崩溃;
2、在某个集中的时间段内,系统预加载的缓存集中失效了;
解决方案:
1、保证缓存层服务高可用性,如使用Redis Sentinel 和 Redis Cluster,双机房部署,保证Redis服务高可用。
2、通过设置不同的过期时间,来错开缓存过期,从而避免缓存集中失效。

4.9 Redis倾斜

技术栈 - 图17
现象:
Hot key,即热点 key,指的是在一段时间内,该 key 的访问量远远高于其他的 redis key, 导致大部分的访问流量在经过 proxy 分片之后,都集中访问到某一个 redis 实例上。hot key 通常在不同业务中,存储着不同的热点信息。比如

  1. 新闻应用中的热点新闻内容;
  2. 活动系统中某个用户疯狂参与的活动的活动配置;
  3. 商城秒杀系统中,最吸引用户眼球,性价比最高的商品信息;

big key ,即数据量大的 key ,由于其数据大小远大于其他key,导致经过分片之后,某个具体存储这个 big key 的实例内存使用量远大于其他实例,造成,内存不足,拖累整个集群的使用。big key 在不同业务上,通常体现为不同的数据,比如:

  1. 论坛中的大型持久盖楼活动;
  2. 聊天室系统中热门聊天室的消息列表;

解决方案:
\1. 使用本地缓存
在 client 端使用本地缓存,从而降低了redis集群对hot key的访问量,但是同时带来两个问题:1、如果对可能成为 hot key 的 key 都进行本地缓存,那么本地缓存是否会过大,从而影响应用程序本身所需的缓存开销。2、如何保证本地缓存和redis集群数据的有效期的一致性。
针对这两个问题,先不展开讲,先将第二个解决方案。
\2. 利用分片算法的特性,对key进行打散处理
我们知道 hot key 之所以是 hot key,是因为它只有一个key,落地到一个实例上。所以我们可以给hot key加上前缀或者后缀,把一个hotkey 的数量变成 redis 实例个数N的倍数M,从而由访问一个 redis key 变成访问 N M 个redis key。
N
M 个 redis key 经过分片分布到不同的实例上,将访问量均摊到所有实例。
\3. 对 big key 进行拆分
对 big key 存储的数据(big value)进行拆分,变成value1,value2… valueN,
如果big value 是个大json 通过 mset 的方式,将这个 key 的内容打散到各个实例中,减小big key 对数据量倾斜造成的影响。
如果big value 是个大list,可以拆成将list拆成= list_1, list_2, list3, listN
其他数据类型同理。
\4. 既是big key 也是 hot key
在开发过程中,有些 key 不只是访问量大,数据量也很大,这个时候就要考虑这个 key 使用的场景,存储在redis集群中是否是合理的,是否使用其他组件来存储更合适;如果坚持要用 redis 来存储,可能考虑迁移出集群,采用一主一备(或1主多备)的架构来存储。

4.10 Redis如何发现hot和big key

\1. 事前-预判在业务开发阶段,就要对可能变成 hot key ,big key 的数据进行判断,提前处理,这需要的是对产品业务的理解,对运营节奏的把握,对数据设计的经验。
2.事中-监控和自动处理
监控
在应用程序端,对每次请求 redis 的操作进行收集上报;不推荐,但是在运维资源缺少的场景下可以考虑。开发可以绕过运维搞定);
在proxy层,对每一个 redis 请求进行收集上报;(推荐,改动涉及少且好维护);
对 redis 实例使用monitor命令统计热点key(不推荐,高并发条件下会有造成redis 内存爆掉的隐患);
机器层面,Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。如果站在机器的角度,可以通过对机器上所有Redis端口的TCP数据包进行抓取完成热点key的统计(不推荐,公司每台机器上的基本组件已经很多了,别再添乱了);
自动处理
通过监控之后,程序可以获取 big key 和 hot key,再报警的同时,程序对 big key 和 hot key 进行自动处理。或者通知程序猿利用一定的工具进行定制化处理(在程序中对特定的key 执行前面提到的解决方案)

4.11 Redis为什么快

1.Redis是纯内存数据库
一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。
2.Redis使用的是非阻塞IO
IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
3.Redis内部使用文件事件处理器 file event handler
这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
在Redis4.0之前,Redis是单线程运行的,但单线程并不代表效率低,像Nginx、Nodejs也是单线程程序,但是它们的效率并不低。但Redis在4.0以及之后的版本中引入了惰性删除(也叫异步删除),就是我们可以使用异步的方式对Redis中的数据进行删除操作,这样处理的好处是不会使Redis的主线程卡顿,会把这些操作交给后台线程来执行
4.Redis全程使用hash结构,读取速度快
5.Redis采用自己实现的事件分离器
效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。

RabblitMQ

rabbitMQ简介 【原理】

严格的遵循 AMQP 协议,高级消息队列协议,帮助我们在进程之间传递异步消息 解耦 削峰填谷 异步
RabbitMQ 名词解释:
Publisher-生产者: 发布消息到 Exchange。
Consumer-消费者:监听 RabbitMq 中的 Queue 中的消息
Exchange-交换机: 和生产者建立连接并 publisher 传来的消息。
Queue-队列: Exchange 会将消息分发到指定的 Queue,Queue 和消费者之间,产生 联系
Routes-路由:交换机以什么样的策略将消息发布到队列中去。

RabbitMQ 几种常见的架构:
  1. Hello-World(简单模式): 一个生产者,一个默认的交换机,一个队列,和一个消费者。
    2. Worker-Queue(工作模式): 一个生产者,一个默认的交换机,一个队列,多个消费者
    3. Publishe/Subscribe(发布订阅模式): 一个生产者,一个交换机(类型是:FANOUT),多 个队列(两个队列中的数 据一致),两个消费者。
    4. 路由模式(Routes): 一个生产者,一个交换机(类型是:DIRECT),两个队列,多个消费者。 根据某种路由规则,交换机把消息存到不同的对列中,每个队列有对应的一个 消费者。
    5. 主题模式(Topic): 一个生产者,一个交换机(类型是:topic),两个队列,多个消费者。 交换机根据 topic 的规则,把不同类型的消息分配到不同的队列上。
    如何保证消息不丢失
    【简短的回答】
    1. Consumer处理信息,完成业务后,手动ack方式确认任务已经处理完毕,如果没有 确认,队列会重新发送同一条消息《手动ack机制》
    2. 生产者发送给交换机信息,必须要得到交换机的确定,保证到达交换机《confirm 机制》
    3. 交换机向队列中投递信息是,如果投递不成功,消息就会回退到return方法。 《return 机制》

【起因】:
有一定的概率会出现消息丢失,比如下面的各种情况:
1.比如消息刚从发送者发送到服务器中,发送者宕机了,在发送的一瞬间,服务器网络异常
2.比如消费者刚从服务器获取数据,还没获取完,服务器宕机了,或者网络延迟了
3.任何一款的MQ都有可能遇到消息丢失的问题,特别是高并发下
【发送端的解决】:
考虑的问题:
1.保证消息发送成功
2.保证服务端接收成功
3.要采用应答模式 发送端和服务端的应答模式
4.需要考虑消息补偿机制
解决方案:
1.消息开启本地持久化
实现消息的落库的处理
2.实现基于延迟的二次确认机制
消息采用2次投递方案,第一次立即执行,第二次间隔执行 采用延迟的模式进行,第二次的投递是为了保证
数据的校验。如果存在就不做处理,要是不存在就同步一次
3.RabbitMQ支持消息事务
可以采用事务的模式来处理消息是否成功
【消费端的解决】
1.保证消息的获取并应答
结合RabbitMQ的消息应答模式 实现消息消费的应答
2.保证消息过重
每一个消息都有唯一ID,每当消费者获取到消息的时候,先检验此消息有没有被消费过,如果没有再继续进行
操作。要是之前消费过,那么就不能再次处理!可以结合Redis

RabbitMQ 如何保证消息不重复?

【简短的回答】
同一条消息,消费者得到两次,正常处理两次、
1 为所有要发送的消息设置唯一的id,存放在redis数据库中
2 在消费者接受消息判断id时,先判断在redis中是否存在,若存在代表已经处理过了, 不需要再次执行,如果id不存在,代表第一次处理

【幂等性】
无论一个消息,接收多少次,也不会出现重复消费的问题比如:支付,用户手速过快,导致快速的多次的发起支付请求,但是只能有一次进行。
MQ有可能会出现一个消息发送多次,导致消息的重复性
什么情况下,会出现消息的重复?
1.网络抖动
2.网络闪断
3.端点异常 服务端或者发送端或者消费端
消息出现非幂等性,就会导致消息的重复
【解决方案】
1.唯一ID+指纹码
唯一ID 可以使用:雪花算法等等 这种分布式唯一ID生成器
指纹码:就是一段密文,加密规则各不相同
常用:时间戳+随机码+唯一ID+业务ID 采用一定加密技术 进行密文生成
2.基于Redis的原子性实现
Redis的原子性,自增啥的都可以,关键Redis支持集群

RabbitMQ 如何保证按顺序执行?

消息队列中的若干消息如果是对同一个数据进行操作,这些操作具有前后的关系, 必须要按前后的顺序执行,否则就会造成数据异常。
1. 可以拆分 多个队列,把要按顺序执行的信息发布到队列中,然后让一个消费者去 消费。
2. 一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排 队,然后分发给底层不同的worker来处理

Mysql

基本知识

缓存设计

内存中的一块存储空间,服务于某个应用程序,旨在将频繁读取的数据临时保存在内存中,便于二次快速访问。
image.png
image.png
一级缓存
一级缓存是SqlSession级别的缓存,同一个SqlSession的发起多次同构查询,会将数据保存在一级缓存中。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
注意:无需任何配置,默认开启一级缓存
image.png
二级缓存
二级缓存
SqlSessionFactory级别的缓存,同一个SqlSessionFactory构建的SqlSession发起的多次同构查询,会将数据保存在二级缓存中
注意:在sqlSession.commit()或者sqlSession.close()之后生效
索引
create index 索引名 on 表名(字段名) 创建索引
explain sql语句 查询索引是否生效
【基本概念】
数据库:存储数据的仓库,软件专门用来存储数据
常用数据库软件:Mysql、Sql Server、Oracle、Postgresql、H2、Sqlite、Db2等等
OLD-SQL NO-SQL NEW-SQL
目前市场最为流行:Mysql
SQL:结构化查询语言
操作数据库·语言,目前SQL99标准
目前SQL的分类:
1.DDL:数据定义语言
关键字:create drop alter
2.DML:数据操作语言
关键字:insert update delete
3.DQL:数据查询语言
关键字:select
4.DCL:数据控制语言
关键字:grant revoke
5.TPL:数据事务语言
关键字:begin transaction、commit 、rollback
6.CCL:指针控制语言
关键字:cursor 游标

事务

【ACID】
原子性:一个事务要么全部提交成功,要么全部失败回滚,不能只执行其中的一部分操作,这就是事务的原子性
一致性:事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。
隔离性:事务的隔离性是指在并发环境中,并发的事务时相互隔离的,一个事务的执行不能不被其他事务干扰。不同的事务并发操作相同的数据时,每个事务都有各自完成的数据空间,即一个事务内部的操作及使用的数据对其他并发事务时隔离的,并发执行的各个事务之间不能相互干扰。
持久性:一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中。

【事务的执行流程】
事务常说一系列操作作为一个整体要么都成功要么都失败,主要特性acid,事务的的实现主要依赖两个log redo-log,undo-log,每次事务都会记录数据修改前的数据undo-log,修改后的数据放入redo-log,提出成功则使用redo-log 更新到磁盘,失败则使用undo-log将数据恢复到事务之前的数据

事务引发问题与事务的隔离级别

脏读:一个事务读取到了另一个事务尚未提交的数据。
不可重复读:事务A读取到了age的值20,事务B将该值修改成了28,事务A再次读取age的值28,事务A两次读取的age值不一致。因为修改导致的
幻读:事务一读取到A表中有一条记录,事务二往A表中插入一条记录,事务一再次读取的时候记录变成了两条,就像发生幻觉一样。因为新增或删除导致的

数据库内部定义了四种隔离级别,用于解决三种隔离问题
1 Serializable:可避免脏读、不可重复读、虚读情况的发生。(串行化)
2 Repeatable read:可避免脏读、不可重复读情况的发生。(可重复读)不可以避免虚读
3 Read committed:可避免脏读情况发生(读已提交)
4 Read uncommitted:最低级别,以上情况均无法保证。(读未提交)

select的执行流程

当执行Select语句时,DBMS的执行步骤表示如下:
(1)执行FORM子句,根据FROM子句中的一个或多个表创建工作表。如果在FROM子句中有两个或多个表,DBMS将对表进行交叉连接,作为工作表。
(2)如果有WHERE子句,DBMS将WHERE子句列出的搜索条件作用于步骤(1)生成的工作表。DBMS将保留那些满足搜索条件的行,删除那些不满足搜索条件的行。
(3)如果有GROUP BY子句,DBMS将步骤(2)生成的结果表中的行分成多个组,每个组所有行的group_by_expression字段具有相同的值,DBMS将每组减少到单行,然后将其添加到新的结果表中。
注:
DBMS将NULL值看作是相等的,而且把所有NULL值都放入其自己的组中。
(4)如果有HAVING子句,DBMS将HAVING子句列出的搜索条件作用于步骤(3)生成的“组合”表中的每一行。DBMS将保留那些满足搜索条件的行,删除那些不满足搜索条件的行。
(5)将SELECT子句作用于结果表。删除结果表中不包含在select_list中的列。如果SELECT子句包含DISTINCT关键字,DBMS将从结果中删除重复的行。
(6)如果有ORDER BY子句,按指定的排序规则对结果进行排序。
(7)对于交互式的SELECT语句,在屏幕上显示结果,对于嵌入式SQL,使用游标将结果传递给宿主程序中。
以上就是SQL SELECT语句的基本执行过程。

联接查询

1、内联接(典型的联接运算,使用像 = 或 <> 之类的比较运算符)。包括相等联接和自然联接。
内联接使用比较运算符根据每个表共有的列的值匹配两个表中的行。例如,检索 students和courses表中学生标识号相同的所有行。

2、外联接。外联接可以是左向外联接、右向外联接或完整外部联接。
在 FROM子句中指定外联接时,可以由下列几组关键字中的一组指定:
1)LEFT JOIN或LEFT OUTER JOIN
左向外联接的结果集包括 LEFT OUTER子句中指定的左表的所有行,而不仅仅是联接列所匹配的行。如果左表的某行在右表中没有匹配行,则在相关联的结果集行中右表的所有选择列表列均为空值。
2)RIGHT JOIN 或 RIGHT OUTER JOIN
右向外联接是左向外联接的反向联接。将返回右表的所有行。如果右表的某行在左表中没有匹配行,则将为左表返回空值。
3)FULL JOIN 或 FULL OUTER JOIN
完整外部联接返回左表和右表中的所有行。当某行在另一个表中没有匹配行时,则另一个表的选择列表列包含空值。如果表之间有匹配行,则整个结果集行包含基表的数据值。

3、交叉联接
交叉联接返回左表中的所有行,左表中的每一行与右表中的所有行组合。交叉联接也称作笛卡尔积。
FROM 子句中的表或视图可通过内联接或完整外部联接按任意顺序指定;但是,用左或右向外联接指定表或视图时,表或视图的顺序很重要。有关使用左或右向外联接排列表的更多信息,请参见使用外联接。

mysql乐观锁和悲观锁

【悲观锁】
当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。

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

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

mysql搜索引擎

【种类】innoDB(默认),myISAM( 一撒木) ,memory

【区别】
InnoDB 支持事物,而 MyISAM 不支持事物
InnoDB 支持行级锁,而 MyISAM 支持表级锁
所有 InnoDB 的并发性要比 myISAM 好
innoDB 支持 mvcc InnoDB 支持外键,而 MyISAM 不支持
InnoDB 不支持全文索引,而 MyISAM 支持。

  1. Mvcc:就是多版本控制,主要就是针对于读已提交,可重复读,这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程。可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
  2. InnoDb 每一行记录都有两个隐藏列 事务 id,和 回滚指针,如果没有主键还会 有一个 隐藏主键 快照读就是读取数据的时候会根据一定规则读取事务可见版本的数据(可能是过 期的数据),不用加锁。当前读, 读取的是最新版本, 并且对读取的记录加锁, 保证其他事务不会再并发的修改这条记录,避免出现安全问题。

索引

create index 索引名 on 表名(字段名) 创建索引
explain sql语句 查询索引是否生效

【索引的分类】:聚簇索引,覆盖索引,联合索引,最左匹配

索引是对数据库表中一列或多列的值进行排序的一种结构,主要目的就是加快检索表中数据,能够帮助我们更快的查找到符合的数据。

主键索引
唯一索引
非聚集索引 :就是索引存储在一个地方 ,数据存储到另一个地方,索引带有 指针 指向数据存储的地方,非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。
聚簇索引 是按照数据存放的物理位置为顺序的, 聚簇索引能提高多行检索的速度,而非聚簇索引对于单行的检索很快。

索引好处:
1. 能提高数据的搜索及检索速度。
2. 能够加快表与表之间的连接速度。
索引坏处:
1. 需花费较多的时间去建立并维护索引,特别是随着数据总量的增加,所花 费的时间将不断递增。
2. 在对表中的数据进行修改时,例如对其进行增加、删除或者是修改操作时, 索引还需要进行动态的维护。会带来一定的麻烦。
3. 会使写入数据的速度下降,因为增删改。会改变平衡树各节点中的索引数 据内容,破坏树结构,dmbs 会重新维护 数据结构

【索引命中规则】
最左匹配原则
1、先定位该sql的查询条件,有哪些,那些是等值的,那些是范围的条件。
2、等值的条件去命中索引最左边的一个字段,然后依次从左往右命中,范围的放在最后。

索引的创建

1.在经常需要搜索的列上,可以加快索引的速度2.主键列上可以确保列的唯一性
3.在表与表的而连接条件上加上索引,可以加快连接查询的速度
4.在经常需要排序(order by),分组(group by)和的distinct 列上加索引 可以加快排序查询的时间, (单独order by 用不了索引,索引考虑加where 或加limit)
5.在一些where 之后的 < <= > >= BETWEEN IN 以及某个情况下的like 建立字段的索引(B-TREE)
6.like语句的 如果你对nickname字段建立了一个索引.当查询的时候的语句是 nickname lick ‘%ABC%’ 那么这个索引讲不会起到作用.而nickname lick ‘ABC%’ 那么将可以用到索引
7.索引不会包含NULL列,如果列中包含NULL值都将不会被包含在索引中,复合索引中如果有一列含有NULL值那么这个组合索引都将失效,一般需要给默认值0或者 ‘ ‘字符串
8.使用短索引,如果你的一个字段是Char(32)或者int(32),在创建索引的时候指定前缀长度 比如前10个字符 (前提是多数值是唯一的..)那么短索引可以提高查询速度,并且可以减少磁盘的空间,也可以减少I/0操作.
9.不要在列上进行运算,这样会使得mysql索引失效,也会进行全表扫描
10.选择越小的数据类型越好,因为通常越小的数据类型通常在磁盘,内存,cpu,缓存中 占用的空间很少,处理起来更快
11.查询中很少使用到的列 不应该创建索引,如果建立了索引然而还会降低mysql的性能和增大了空间需求.12.很少数据的列也不应该建立索引,比如 一个性别字段 0或者1,在查询中,结果集的数据占了表中数据行的比例比较大,mysql需要扫描的行数很多,增加索引,并不能提高效率
13.定义为text和image和bit数据类型的列不应该增加索引,
14.当表的修改(UPDATE,INSERT,DELETE)操作远远大于检索(SELECT)操作时不应该创建索引,这两个操作是互斥的关系

行级锁 表级锁 页级锁

1.行级锁(是锁索引)是 mysql 中锁定粒度最细的一种锁。
表示只针对当前操作的行 进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开 销也最大。行级锁分为共享锁和排他锁
特点:开销大,加锁慢,会出现死锁。发生锁冲突的概率最低,并发度也最高。

2 表级锁:
表级锁是 mysql 中锁定粒度最大的一种锁,表示对当前操作的整张 表加锁,它实现简单,资源消耗较少,被大部分 mysql 引擎支持。最常使用的 MyISAM 与 InnoDB 都支持表级锁定。
特点:开销小,加锁快,不会出现死锁。发生锁冲突的概率最高,并发度也最低。

3 页级锁:页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。
表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁, 一次锁定相邻的一组记录。
特点:开销和加锁时间界于表级锁和行级锁之间;会出现死锁;锁定粒度界于表 级锁和行级锁之间,并发度一般。

B+Tree的实现原理

索引底层是基于B+Tree进行实现的
B+Tree旨在叶子节点存放数据,在根节点无数据只有指向指针

B树和 B+树的区别?

【B-Tree】
mysql默认存储引擎innodb只显式支持B-Tree( 从技术上来说是B+Tree)索引,对于频繁访问的表,innodb会透明建立自适应hash索引,即在B树索引基础上建立hash索引,可以显著提高查找效率,对于客户端是透明的,不可控制的,隐式的。
B-Tree能加快数据的访问速度,因为存储引擎不再需要进行全表扫描来获取数据,数据分布在各个节点之中

【B+Tree】
是B-Tree的改进版本,同时也是数据库索引所采用的存储结构。数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。相比B-Tree来说,进行范围查找时只需要查找两个节点,进行遍历即可。而B-Tree需要获取所有节点,相比之下B+Tree效率更高。

B 树和 B+树都是多叉树,是改变二叉树结构的高度较大进行优化的(树的高度较大不适合存储海量数据)。 B+树只有叶子节点存放数据,根节点无数据(只有)指向指针(占用 6byte), 这样的好处是在高度相等的树结构中,可以存放更多的数据。
B+树在叶子节点之间增加了双向链表指针,对于范围查询,有更高的效率

sql优化步骤:

【定位sql】
1.慢查询,在sql配置文件中进行配置时间,如果sql语句执行时间超过某个时间的话,就判定为慢查询,记录到日志中。
2.AOP实现
3.druid的sql监控
4.第三方平台,如evweSQL
【分析sql语句慢的原因】
1.并发量 2.数据量 3.最短路径 4.索引(重点)—复合索引 最左前缀原则 5.计算 6.冗余
【解决】
1.并发量—
1.程序中数据库连接池 控制有效连接,提高连接的复用率
2.扩容-Mysql服务器 —搭建Mysql集群
3.数据库读写分离 查询多 —Mycat
写库(主库)—->InnoDB ,
读库(从库)——>Myisma
二进制日志
2.数据量
1.数据分片 垂直分片 水平分片 Mycat— 分片算法
3.最短路径
1.分析表关系 关系型数据库 最短关系
2.梳理sql语句
4.索引(重点)—复合索引 最左前缀原则
1.索引是否合适 之前是否有过相关索引 冗余
2.索引的数量
3.索引生效
4.索引 复合索引的字段顺序 最左匹配原则
索引在那些情况下会失效:自己验证
5.覆盖索引
5.计算
1.避免在查询字段上做运算
2.避免条件查询字段做运算
6.冗余
1.sql语句存在冗余 需要结合业务逻辑
2.保证网络环境

索引优化的方式

1、你必须选择记录条数最少的表作为基础表
from 是从前往后检索的,所以要最少记录的表放在最前面。
2、采用自下而上的顺序解析WHERE子句,根据这个原理,表之间的连接必须写在其他WHERE条件之前, 那些可以过滤掉最大数量记录的条件必须写在WHERE子句的末尾。同时在链接的表中能过滤的就应该先进行过滤。
where是从后往前检索,所以能过滤最多数据的条件应放到最后。
3、SELECT子句中避免使用 ‘
4、尽量多使用COMMIT
5、计算记录条数时候,第一快:count(索引列),第二快:cout(
)
6、用WHERE子句替换HAVING子句
7、通过内部函数提高SQL效率
8、使用表的别名(Alias)
9、用EXISTS替代IN
10、用NOT EXISTS替代NOT IN
11、用表连接替换EXISTS
12、用索引提高效率
13、尽量避免在索引列上使用计算,
包括在SELECT后面 WHERE后面等任何地方,因为在索引列上计算会导致索引失效。
14、避免在索引列上使用NOT
在索引列使用not会导致索引失效。
15、用>=替代>
16、用UNION替换OR (适用于索引列)
17、用IN来替换OR
18、避免在索引列上使用IS NULL和IS NOT NULL
19、总是使用索引的第一个列
20、尽量用UNION-ALL 替换UNION ( 如果有可能的话)
21、ORDER BY 子句只在两种严格的条件下使用索引.
22、避免改变索引列的类型
23、需要当心的WHERE子句
24、避免使用耗费资源的操作(如DISTINCT,UNION,MINUS,INTERSECT,ORDER BY等)

什么情况 会导致 索引失效?
  1. 索引列中 使用了运算或者函数
    2. Or 语句中前后字段 没有同时都为索引
    3. 数据类型出现隐式转换,例如字符串中没有使用 单引号
    4. Like语句以 % 开头
    5. 索引字段中 使用 isa null,is not null, !,= ,<>
    6. 复合索引 中没有 遵循最佳 左前缀 原则
    7. 查询结果大于 全表的 30%
    主从复制
    读写分离
    分库分表【使用第三方mycat实现】
    【概念】分库分表就是为了解决由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库组成 ,将数据大表拆分成若干数据表组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。
    【分类】
    【垂直分表】 将一个表按照字段分成多表,每个表存储其中一部分字段。
    优点:
    1.为了避免IO争抢并减少锁表的几率,查看详情的用户与商品信息浏览互不影响
    2.充分发挥热门数据的操作效率,商品信息的操作的高效率不会被商品描述的低效率所拖累。
    原则:
    把不常用的字段单独放在一张表;
    把text,blob等大字段拆分出来放在附表中;
    经常组合查询的列放在一张表中;
    【垂直分库】 垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用。
    优点:
    解决业务层面的耦合,业务清晰
    能对不同业务的数据进行分级管理、维护、监控、扩展等
    高并发场景下,垂直分库一定程度的提升IO、数据库连接数、降低单机硬件资源的瓶颈
    垂直分库通过将表按业务分类,然后分布在不同数据库,并且可以将这些数据库部署在不同服务器上,从而达到多个服务器共同分摊压力的效果,但是依然没有解决单表数据量过大的问题。
    【水平分库】 把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。
    优点:
    解决了单库大数据,高并发的性能瓶颈。
    提高了系统的稳定性及可用性
    【水平分表】 是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中。
    优点:
    优化单一表数据量过大而产生的性能问题
    避免IO争抢并减少锁表的几率
    库内的水平分表,解决了单一表数据量过大的问题,分出来的小表中只包含一部分数据,从而使得单个表的数据量变小,提高检索性能。

ES

ES的
ElasticSearch是基于Lucene开发的分布式搜索框架,包含如下特性:
1. 分布式索引、搜索。
2. 索引自动分片、负载均衡。
3. 自动发现机器、组建集群。
4. 支持Restful 风格接口。
5. 配置简单等。
6. 海量数据的存储。

分布式

分布式事务

前端概念:
CAP 理论:
C:一致性,A:可用性,P:分区容错性 一致性(Consistency)、可用性(Availability)、分区容错性(Partitiontolerance) 在分布式,跨应用的业务只能满足其中两个。
Eureka(注册中心):
AP,保证了可用性和分区容错性,舍弃了一致性, Eureka 是 Netflix 开发的服务发现框架 Zookeeper:CP,每一个节点必须能够和 Master进行通信,才能对外提供 服务,舍弃了可用性。 保证一致性,分区容错 ,可用性降低。
Base 理论,BA:基本可用,S:中间状态,E:最终一致性。
Base 是对 CAP 理论的妥协,业务开始可以,可以没有一致性,先保证可用性,通过一定的时间,使用补偿机制到最用一致性。
两阶段提交(2pc)
第一阶段:每个服务都开启事务并执行 sql,并检查状态,将结果反馈给事务 管理器
第二阶段:事务管理器(TM)根据每个服务返回的事务状态,决定所有事务是 否提交或者回滚


seata 有那些模块组成?
TM: 事务管理器 (发送 命令)
TC: 事务协调者 协调 传递 提交/回滚命令
RM: 资源管理器(子事务)
执行流程
1.TM 向 Tc 申请 事务 id
2.事务 id 在 TC 这个流程传播
3.RM 向 TC 注册当前 事务 id,(将本地事务挂载到 对应的事务 id 下)
4.TM 发起事务提交
5.TC 协调所有的 RM 进行提交,有问题,回滚

seata是怎么实现回滚的?

分布式锁

什么是分布式锁?

在应用内保证线程安全(数据安全),能够按照代码逻辑正确执行分布式锁:在多个应用中,共享资源,保证资源的在多线程,多应用访问时,数 据安全

实现分布式锁的方式?

1.redis——-》 setnx( 当 key 不存在设置 key 成功,如果 key 存在,设置失败 ) 作为分布式锁
在项目中具体的实现的话,使用
2.使用 zk 实现 ——》临时有序节点 —-》curator
3.使用 mysql 数据库 某一行的标记位 来实现

为什么使用redis锁

1.redis 性能高 ,防止死锁 (setnx 设置时间)
2.zk 基于分布式文件实现,速度相对来说,很慢,有可能会发生死锁
如果我们的系统要部署在多台服务器上,那么就需要考虑使用分布式锁
synchronized或者lock 单台服务器
分布式锁的实现方案:
1.基于Redis实现分布式锁 setnx
2.基于Zookeeper(注册中心)实现分布式锁
3.基于数据库(悲观锁—排它锁或者乐观锁)
选择Redis,用的Redisson—-封装了RedLock(红锁)
比如:下单购买商品的时候
只要在很多个用户同时购买同一个商品的时候再加锁
//获取分布式锁对象
RLock rlock=RedissonUtil.rlock(RedisKeyConfig.ORDER_LOCK+dto.getGid());
//加锁
rlock.lock(RedisKeyConfig.ORDER_LOCK_TIME, TimeUnit.SECONDS);
//验证是否加锁成功
if(rlock.isLocked()){
//执行代码
try {
//下单逻辑
}finally {
//释放分布式锁
rlock.unlock();
}
}

分布式组件

注册中心 配置中心 nacos
网关 getway
远程服务调用 openfigen
链路跟踪
熔断降级

网络基础

TCP/IP五层协议

技术栈 - 图21

应用层协议?解决了什么问题?DNS/HTTP

应用层(application layer):是体系结构中的最高。直接为用户的应用进程(例如电子邮件、文件传输和终端仿真)提供服务。
在因特网中的应用层协议很多,如支持万维网应用的HTTP协议,支持电子邮件的SMTP协议,支持文件传送的FTP协议,DNS,POP3,SNMP,Telnet等等。

传输层协议?解决了什么问题?说说TCP/UDP的特点

运输层(transport layer):负责向两个主机中进程之间的通信提供服务。由于一个主机可同时运行多个进程,因此运输层有复用和分用的功能
复用,就是多个应用层进程可同时使用下面运输层的服务。
分用,就是把收到的信息分别交付给上面应用层中相应的进程。
运输层主要使用以下两种协议:
(1) 传输控制协议TCP(Transmission Control Protocol):面向连接的,数据传输的单位是报文段,能够提供可靠的交付。
(2) 用户数据包协议UDP(User Datagram Protocol):无连接的,数据传输的单位是用户数据报,不保证提供可靠的交付,只能提供“尽最大努力交付”。

TCP报文首部(序号/确认号/SYN/ACK;FIN)

TCP报文是TCP层传输的数据单元,也叫报文段。

1、端口号:用来标识同一台计算机的不同的应用进程。
1)源端口:源端口和IP地址的作用是标识报文的返回地址。
2)目的端口:端口指明接收方计算机上的应用程序接口。

TCP报头中的源端口号和目的端口号同IP数据报中的源IP与目的IP唯一确定一条TCP连接。

【序号和确认号】:是TCP可靠传输的关键部分。序号是本报文段发送的数据组的第一个字节的序号。在TCP传送的流中,每一个字节一个序号。e.g.一个报文段的序号为300,此报文段数据部分共有100字节,则下一个报文段的序号为400。所以序号确保了TCP传输的有序性。确认号,即ACK,指明下一个期待收到的字节序号,表明该序号之前的所有数据已经正确无误的收到。确认号只有当ACK标志为1时才有效。比如建立连接时,SYN报文的ACK标志位为0。

【控制位】:URG ACK PSH RST SYN FIN,共6个,每一个标志位表示一个控制功能。
1)URG:紧急指针标志,为1时表示紧急指针有效,为0则忽略紧急指针。
2)ACK:确认序号标志,为1时表示确认号有效,为0表示报文中不含确认信息,忽略确认号字段。
3)PSH:push标志,为1表示是带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将这个报文段交给应用程序,而不是在缓冲区排队。
4)RST:重置连接标志,用于重置由于主机崩溃或其他原因而出现错误的连接。或者用于拒绝非法的报文段和拒绝连接请求。
5)SYN:同步序号,用于建立连接过程,在连接请求中,SYN=1和ACK=0表示该数据段没有使用捎带的确认域,而连接应答捎带一个确认,即SYN=1和ACK=1。
6)FIN:finish标志,用于释放连接,为1时表示发送方已经没有数据发送了,即关闭本方数据流。

三次握手讲下流程

【建立tcp连接时】
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

四次挥手讲下流程?

【关闭tcp连接时使用】
对于一个已经建立的连接,TCP使用改进的三次握手来释放连接(使用一个带有FIN附加标记的报文段)。TCP关闭连接的步骤如下:

第一步,当主机A的应用程序通知TCP数据已经发送完毕时,TCP向主机B发送一个带有FIN附加标记的报文段(FIN表示英文finish)。
第二步,主机B收到这个FIN报文段之后,并不立即用FIN报文段回复主机A,而是先向主机A发送一个确认序号ACK,同时通知自己相应的应用程序:对方要求关闭连接(先发送ACK的目的是为了防止在这段时间内,对方重传FIN报文段)。
第三步,主机B的应用程序告诉TCP:我要彻底的关闭连接,TCP向主机A送一个FIN报文段。
第四步,主机A收到这个FIN报文段后,向主机B发送一个ACK表示连接彻底释放。

为什么使用三次握手和四次挥手

为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

在浏览器发起API调用到得到返回结果,这中间经历的流程都有哪些?

:首先通过DNS域名解析出服务器IP,然后通过TCP/IP协议建立起客户端到服务器的TCP连接。网络连接建立好之后,通过HTTP协议发出请求包,从网络模型的应用层传到物理层,随后发送出去。请求包在服务器端经过负载均衡,到达服务器,经服务器处理后返回结果给客户端,客户端收到结果后进行页面渲染,最终展示到浏览器上。

常用的HTTP状态码/HTTP通用首部/请求首部/响应首部

【2xx】 请求处理完毕 (3种)
200 OK:表示从客户端发送给服务器的请求被正常处理并返回;
204 No Content:表示客户端发送给客户端的请求得到了成功处理,但在返回的响应报文中不含实体的主体部分(没有资源可以返回);
206 Patial Content:表示客户端进行了范围请求,并且服务器成功执行了这部分的GET请求,响应报文中包含由Content-Range指定范围的实体内容。

【3xx】 需要进行附加操作完成请求 (5种)
301 Moved Permanently:永久性重定向,表示请求的资源被分配了新的URL,之后应使用更改的URL;
302 Found:临时性重定向,表示请求的资源被分配了新的URL,希望本次访问使用新的URL;
301与302的区别:前者是永久移动,后者是临时移动(之后可能还会更改URL)
303 See Other:表示请求的资源被分配了新的URL,应使用GET方法定向获取请求的资源;
302与303的区别:后者明确表示客户端应当采用GET方式获取资源
304 Not Modified:表示客户端发送附带条件(是指采用GET方法的请求报文中包含if-Match、If-Modified-Since、If-None-Match、If-Range、If-Unmodified-Since中任一首部)的请求时,服务器端允许访问资源,但是请求为满足条件的情况下返回改状态码;
307 Temporary Redirect:临时重定向,与303有着相同的含义,307会遵照浏览器标准不会从POST变成GET;(不同浏览器可能会出现不同的情况);
【4xx】 客户端请求出错 (4种)
400 Bad Request:表示请求报文中存在语法错误;
401 Unauthorized:未经许可,需要通过HTTP认证;
403 Forbidden:服务器拒绝该次访问(访问权限出现问题)
404 Not Found:表示服务器上无法找到请求的资源,除此之外,也可以在服务器拒绝请求但不想给拒绝原因时使用;
【5xx】 服务端处理请求出错 (2种)
500 Inter Server Error:表示服务器在执行请求时发生了错误,也有可能是web应用存在的bug或某些临时的错误时;
503 Server Unavailable:表示服务器暂时处于超负载或正在进行停机维护,无法处理请求;

车辆租赁项目

项目介绍:
这个项目是一个车辆出租的平台,主要用于用户通过平台租赁车辆,使用到的技术有springcloud springboot、mybatis、redis、mysql、rabbitMQ、阿里云服务(OSS、短信服务)
用户生成订单请求发送之后,请求在网关经过校验后到达订单服务->订单服务执行业务:查询库存(redis),调用优惠服务计算价格(积分、优惠券),之后生成订单同时减少库存(修改车辆租赁状态)(redis),将订单信息返回给用户查看,用于用户进行支付操作。
同时使用rabbitMQ消息队列,异步将订单存储到数据库、减少数据库的库存(修改车辆的租赁状态)、生成日志信息、扣减积分或者消耗优惠券等。
使用的是Worker-Queue(工作模式): 一个队列,多个消费者来执行。消费者处理的业务是相同的,都是调用商品(车俩)服务减少数据库库存(车辆状态),生成订单日志,保存订单信息到数据库、如果使用到积分或优惠券也是在这里进行的。
为了确保消费者的多个服务之间调用的数据一致性,我们使用的是seate来做的一个分布式事务。
在用户进行商品(车辆)操作的时候,例如当一个用户在查询用户状态的时候,是不允许其他用户进行访问的

redis中存储的数据

使用String来存储订单的信息 key 订单id value 订单信息

确保订单id不可重复

在搭建集群的情况下,基于redis实现的订单信息不可重复
String结构 incr key(自增) decr key(自减)

认证服务

用户登录流程

用户输入手机号密码之后,像服务端发送登录请求,请求到达后端后,后端对数据进行校验,校验通过之后,生成token,将token存放到redis中,并设置有效时间XX分钟,同时响应客户端登录成功并携带token,之后用户每次进行操作时将token携带,与redis中的token进行校验,而不需要用户再次登录。
使用的是基于token的鉴权机制完成的认证
1.用 户使用用户名密码来请求服务器
2.服务器进行验证用户的信息
3.服务器通过验证发送给用户一个token
4.客户端存储token,并在每次请求时附送上这个token值
5.服务端验证token值,并返回数据

token的生成 使用的jjwt来进行生成token的

token存放在redis中 存储的数据结构使用的是string类型进行存储,
【token作为key,值是用户id】 用于通过token获取用户的信息
【用户账号作为key,值是token】 用于与用户携带过来的token进行校验

考虑唯一登录【多个设备只能由一个用户登录】

在验证账号密码成功生成token之后,会首先先去redis中查看一下这个用户是否已经登录过了【验证redis中是否已经存在该用户登录之后生成的token信息,存在代表已经登录】
我们判定当有一个新设备登录的时候,会挤掉之前设备的登录信息,就是说更新redis中的token数据

jwt的组成部分

完整的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

头部(header):
头部承载两部分信息:
{
‘typ’: ‘JWT’,//声明类型
‘alg’: ‘HS256’ //声明加密的算法
}
载荷(payload):
标准中注册的声明
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

  1. 公共的声明<br /> 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.<br /> 私有的声明<br /> 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。<br /> <br />签证信息(signature):<br /> 包含三部分:<br /> header (base64后的)<br /> payload (base64后的)<br /> secret<br /> 这个部分需要base64加密后的headerbase64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。<br /> <br />将这三部分用.连接成一个完整的字符串,构成了最终的jwt

【注意】:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

加密

加密建立在对信息进行数学编码和解码的基础上。
加密类型分为两种,对称加密与非对称加密,对称加密双方采用共同密钥,(当然这个密钥是需要对外保密的),这里讲一下非对称加密,这种加密方式存在两个密钥,密钥— 一种是公共密钥(正如其名,这是一个可以公开的密钥值),一种是私人密钥(对外保密)。
您发送信息给我们时,使用公共密钥加密信息。 一旦我们收到您的加密信息,我们则使用私人密钥破译信息密码(被我们的公钥加密的信息,只有我们的私钥可以解密,这样,就在技术上保证了这封信只有我们才能解读——因为别人没有我们的私钥)。 使用私人密钥加密的信息只能使用公共密钥解密(这一功能应用与数字签名领域,我的私钥加密的数据,只有我的公钥可以解读,具体内容参考数字签名的信息)反之亦然,以确保您的信息安全。

1 Base64加密方式(可逆)

Base64中的可打印字符包括字母A-Z/a-z/数组0-9/ 加号’+’斜杠’/’ 这样共有62个字符

Base64 ios7之后加入系统库

2 MD5加密

Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护

是计算机广泛使用的杂凑算法之一(又译摘要算法、哈希算法),主流编程语言普遍已有MD5实现。

根据输出值,不能得到原始的明文,即其过程不可逆

MD5算法具有以下特点:

1、压缩性:任意长度的数据,算出的MD5值长度都是固定的。

2、容易计算:从原数据计算出MD5值很容易。

3、抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。

4、强抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。

MD5的作用是让大容量信息在用数字签名软件签署私人密钥前被”压缩”成一种保密的格式(就是把一个任意长度的字节串变换成一定长的十六进制数字串)。除了MD5以外,其中比较有名的还有sha-1、RIPEMD

以及Haval等。

MD5加盐

3 钥匙串加密方式

iCloud钥匙串,苹果给我们提供的密码保存的解决方案,iOS7之后有的

存沙盒:

1、如果手机越狱,密码容易被窃取。

2、当软件更新时,沙盒里的内容是不被删除的。但是,如果将软件卸载后重装,沙盒里的数据就没有了。

3、每个APP的沙盒是相对独立的,密码无法共用。

存钥匙串里:

1、苹果提供的安全方案,rsa加密,相对安全。

2、无论软件更新或删除,密码都存在,都可以自动登录。

3、同一公司的APP密码是可以共用的。

4 对称加密算法

优点:算法公开、计算量小、加密速度快、加密效率高、可逆

缺点:双方使用相同钥匙,安全性得不到保证

现状:对称加密的速度比公钥加密快很多,在很多场合都需要对称加密,

算法: 在对称加密算法中常用的算法有:DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK、AES等。不同算法的实现机制不同,可参考对应算法的详细资料

相较于DES和3DES算法而言,AES算法有着更高的速度和资源使用效率,安全级别也较之更高了,被称为下一代加密标准

nECB :电子代码本,就是说每个块都是独立加密的
nCBC :密码块链,使用一个密钥和一个初始化向量 (IV)对数据执行加密转换

ECB和CBC区别:CBC更加复杂更加安全,里面加入了8位的向量(8个0的话结果等于ECB)。在明文里面改一个字母,ECB密文对应的那一行会改变,CBC密文从那一行往后都会改变。

5 RSA加密(非对称加密算法)(Secruty.framework系统库)

非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)

非对称加密中使用的主要算法有:RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)等。

公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密

特点:
非对称密码体制的特点:算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快

对称密码体制中只有一种密钥,并且是非公开的,如果要解密就得让对方知道密钥。所以保证其安全性就是保证密钥的安全,而非对称密钥体制有两种密钥,其中一个是公开的,这样就可以不需要像对称密码那样传输对方的密钥了

但是RSA加密算法效率较差,对大型数据加密时间很长,一般用于小数据。