2020西安面试

数组和集合的区别
数组不是面向对象的,存在明显的缺陷,集合完全弥补了数组的一些缺点,比数组更灵活更实用,可大大提高软件的开发效率而且不同的集合框架类可适用于不同场合。
具体如下:
1)数组的效率高于集合类.
2)数组能存放基本数据类型和对象,而集合类中只能放对象。
3)数组容量固定且无法动态改变,集合类容量动态改变。
4)数组无法判断其中实际存有多少元素,length只告诉了array的容量。
5)集合有多种实现方式和不同的适用场合,而不像数组仅采用顺序表方式。
6)集合以类的形式存在,具有封装、继承、多态等类的特性,通过简单的方法和属性调用即可实现各种复杂操作,大大提高软件的开发效率。

接口和抽象类的区别:
1、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。

2、抽象类要被子类继承,接口要被类实现。

3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现

4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。

5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。

6、抽象方法只能申明,不能实现,接口是设计的结果 ,抽象类是重构的结果

7、抽象类里可以没有抽象方法

8、如果一个类里有抽象方法,那么这个类只能是抽象类

9、抽象方法要被实现,所以不能是静态的,也不能是私有的。

10、接口可继承接口,并可多继承接口,但类只能单根继承。

反射是在类的加载阶段进行的,当类被JVM加载为.class文件后,在内存中JVM会开辟内存,生成.class对象,反射是根据class对象反向获取该类的一些信息
类加载机制:加载,验证,准备,解析,初始化
加载:这个阶段会在内存中生成一个代表这个类的java.lang.Class 对
象,作为方法区这个类的各种数据的入口

JVM

JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法栈】、线程共享区域【JAVA 堆、方法区】,直接内存(1.8之后将元空间保存在了直接内存,元空间取代了堆的永久代)。
程序计数器:一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,线程私有,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。
虚拟机栈:每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。在线程创建时创建的,所以生命周期也是和线程生命周期一致,同时消亡,线程结束了栈也就释放,特别提醒的是栈不存在垃圾回收的问题,因为线程结束栈就是释放了。
:存放对象实例和数组。分为新生代(1/3)和老年代(2/3),新生代中采用minorGC复制算法进行垃圾回收,老年代中采用majorGC标记清除和标记整理算法进行垃圾回收。年轻代中包含Eden区和Survivor区,Survivor区包含From(S0)区和To(区)
MinorGC 的过程(采用复制算法,复制->清空->互换):将Eden区、From区中存活的对象复制到to区,同时对象分代年龄+1,然后清空Eden区、From区中的对象,最后to区和From区进行互换,原to区作为下一次GC的from区。当分代年龄达到15次以上,会将这些对象放在老年代中。默认新生代中Eden区、From区、To区的比例为8:1:1。
老年代采用标记清除和标记整理算法进行垃圾回收,老年代内存不足时,会触发Full gc,如果Full gc无法释放足够的空间,会触发OOM内存溢出,在进行Minor gc或Full gc时,会触发STW(Stop The World),即停止用户线程。
方法区:(常量池)线程共享,存储类信息,常量,静态变量等数据,元空间(java8中采用的是本地内存)
垃圾回收算法:标记清除,复制,标记整理
标记清除:标记出要清除的对象再清除,容易造成内存碎片化,空间利用不足
复制:将内存分为同等大小的两块,只使用一块,若满了则复制到另外一块上并清除当前块。内存效率高但可用内存降低。
标记整理:标记出要清除的对象,然后将存活的对象移动到内存一端,然后再清除边界外的对象
分代收集算法:新生代因为要回收大部分对象,所以采用复制算法效率高。老年代因为没有那么多的回收所以采用标记整理算法。
方法区(永久代)的垃圾回收主要收集两部分内容:废弃常量和无用类。

List:有序可重复
linkedlist和arraylist:
ArrayList的实现原理总结如下:
1、数据存储是基于数组实现的,默认初始容量为10;
2、添加数据时,首先需要检查元素个数是否超过数组容量,如果超过了则需要对数组进行扩容;插入数据时,需要将插入点k开始到数组末尾的数据全部向后移动一位。
3、数组的扩容是新建一个大容量(原始数组大小+原始数组大小*0.5扩充容量)的数组,然后将原始数组数据拷贝到新数组,然后将新数组作为扩容之后的数组。数组扩容的操作代价很高,我们应该尽量减少这种操作。
4、删除数据时,需要将删除点+1位置开始到数组末尾的数据全部向前移动一位。
5、获取数据很快,根据数组下标可以直接获取。
linkedlist的实现原理总结如下:
由双向链表组成,每一个节点由前后节点信息和当前节点数据组成,当前节点的前信息指向上一个节点的后节点信息,后信息指向下一个节点的前节点信息
存储时,只需将节点信息更新即可,所以存储快,读取时,要从头节点遍历每一个节点,所以读取慢。

set:hashset,treeset 无序且不可重复 linkedhashset(根据插入顺序排序,不可重复)
hashset:(1)基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。封装了一个HashMap对象来存储所有的集合元素,所有放入HashSet中的集合元素实际上由HashMap的key来保存,
而 HashMap的value则存储了一个PRESENT,它是一个静态的Object对象。
(2)当我们试图把某个类的对象当成 HashMap的 key,或试图将这个类的对象放入 HashSet 中保存时,重写该类的equals(Object obj)方法和 hashCode() 方法很重要,而且这两个方法的返回值必须保持一致:
当该类的两个的 hashCode() 返回值相同时,它们通过 equals() 方法比较也应该返回 true。通常来说,所有参与计算 hashCode() 返回值的关键属性,都应该用于作为 equals() 比较的标准。
(3)HashSet的其他操作都是基于HashMap的。
继承与HashSet、又基于LinkedHashMap来实现的。
LinkedHashSet:LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同,因此LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,
调用父类的构造器,底层构造一个LinkedHashMap来实现,在相关操作上与父类HashSet的操作相同,直接调用父类HashSet的方法即可。

HashMap

底层使用数组实现,数组中每一项是个单向链表,即数组和链表的结合体;当链表长度大于一定阈值时,链表转换为红黑树,这样减少链表查询时间。
Node[] table, Node实现了Map.entry接口,本质是键值对;该数组初始化长度16
Node对象是hashmap的内部类,由key、value、hash、指向下个节点的node对象的next
HashMap就是使用哈希表来存储的,数组加链表的结合为了解决hash碰撞。
put的过程:根据key计算hash值,放入某个桶中,若hash值相等,则比较key值,相同则替换,不同则以链表的方式链接在后面,数组长度超过8且链表长度大于64时则将链表转换成红黑树,低于6则转换成链表。
get的过程:根据key计算hash值,找到对应的桶,再比较key值,在链表中获取到对应的节点信息
LinkedHashMap:haspmap基础上实现链表的互连,所以是有序的
treemap排序:
treemap本身就实现了key的排序,要实现key排序可使用比较器Comparator,Comparator是一个内部类,实现方法即可。要实现value排序,则要使用Collections的sort方法(先转换成entryset对象,排序后再遍历entryset即可)

ConcurrentHashMap底层原理:线程安全
1.7:ReentrantLock+segment+hashentry 分段锁,一个segmen中包含一个hashentry数组,每个hashentry是一个链表结构 查询时先得到segment,再得到hashentry元素链表所对应的头部,再用equals方法对比value,像hashmap。每个segment互不影响分开锁
1.8:synchronized+cas+Node+红黑树 Node的val和next都用volatile修饰,保证可见性 ,锁住Node的head节点,不影响其他元素的访问使用

线程
锁:
乐观锁:读多写少,每次读都不加锁,只有在更新数据时才会判断数据是否一致,在写时才会加锁
悲观锁:单例模式,synchronize就是悲观锁或者ReentrantLock
ReentrantLock 通过方法lock()与unlock()来进行加锁与解锁操作,与synchronized 会被JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,
使用ReentrantLock 必须在finally 控制块中进行解锁操作。ReentrantLock 可中断、公平锁、多个锁,创建Condition 对象来使线程wait,必须先执行lock.lock 方法获得锁,signal方法唤醒

互斥锁,自旋锁

ReadWriteLock读写锁:接口
如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥。读的时候加读锁,写的时候加写锁

线程池

接口ExecutorService
newCachedThreadPool:若可用,重用以前可用的线程
newFixedThreadPool:所有线程处于活动状态,添加附加任务时会在队列中等待,若所有线程失败,则会重新创建一个新线程执行此任务
newScheduledThreadPool:可延迟或定期执行线程任务
newSingleThreadExecutor:创建只有一个线程的线程池
interrupt()方法:要先捕获InterruptedException 异常之后通过break 来跳出循环,才能正常结束run 方法。
sleep和wait的区别:
sleep属于tread的方法,而wait属于object,sleep不会释放对象锁,wait会
start和run的区别:
star让线程处于就绪状态,分到cpu后才执行run方法,可实现多线程运行
run是执行状态,run方法结束后才会线程终止,其他线程需要等待才会执行

当向线程池提交了一个任务之后,线程池的处理流程如下:

1.判断是否达到核心线程数,若未达到,则直接创建新的线程处理当前传入的任务,否则进入下个流程
2.线程池中的工作队列是否已满,若未满,则将任务丢入工作队列中先存着等待处理,否则进入下个流程
3.是否达到最大线程数,若未达到,则创建新的线程处理当前传入的任务,否则交给线程池中的饱和策略进行处理。

代码块的执行顺序:父类静态代码块-子类静态代码块-父类构造代码块-父类构造方法-子类构造代码块-子类构造方法

int和integer:
-128到127时,integer自动拆箱和int比较会相等;两个integer比较时则不同,因为是不用的对象不同的内存地址;new的integer和没new的integer比较也不同,没new的是存储在常量池,new的则在堆里。
超过此数值后则不相等。

get和post的区别:
GET在浏览器回退时是无害的,而POST会再次提交请求。
GET产生的URL地址可以被Bookmark,而POST不可以。
GET请求会被浏览器主动cache,而POST不会,除非手动设置。
GET请求只能进行url编码,而POST支持多种编码方式。
GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
GET请求在URL中传送的参数是有长度限制的,而POST么有。
对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
GET参数通过URL传递,POST放在Request body中。
GET产生一个TCP数据包;POST产生两个TCP数据包。所以post数据更完整些
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
HTTP状态码:
200-299 用于表示请求成功。
201服务端已创建,202服务端已接受正在执行
300-399 用于已经移动的文件并且常被包含在定位头信息中指定新的地址信息。
300请求的资源有多种选择,301被请求的资源已永久移动到新位置,
400-499 用于指出客户端的错误。
400客户端请求的语法错误,401请求要求用户的身份认证
500-599 用于支持服务器错误。
500服务器内部错误,无法完成请求,501服务器不支持请求的功能,无法完成请求,502充当网关或代理的服务器,从远端服务器接收到了一个无效的请求,505服务器不支持请求的HTTP协议的版本,无法完成处理
转发(Forward)和重定向(Redirect)的区别?
转发是服务器行为,重定向是客户端行为。
Cookie和Session的的区别?
Session是保存在服务端的,Cookie存放在客户端。
单点登录系统
独立的系统,数据库中根据查询用户信息,获得正确用户后,生成一个token,存放在redis库中。
key是token,value是用户信息
设置过期时间
将token信息存放在cookie中
其他系统登录时,先是获取本地存放的cookie,对比SSO中的token是否存在
若存在,则对比token是否相同
不相同则生成新的token,更新redis缓存库,并提示再次登录
若相同,则使用获取的token信息在redis中查询用户信息直接登录即可

spring的AOP
应用: 日志,事务管理,异常处理。都可作为增强功能织入相关业务逻辑中
spring的AOP是运行时的方式织入,而aspectj是采用编译时的方式织入(字节码),spring集成了aspectj去进行AOP。aspectj也是一种AOP框架
Spring的AOP实现是通过JDK代理和CGlib代理,可实现事务管理,权限,日志等
而在spring中使用aspectj实现AOP时则更细节化
比如要增强某个功能,则使用spring的AOP思想,在接口或父类中实现即可,若要以aspectj的方式实现,则需要配置切点切面通知等,采用XML的声明方式或注解方式都行,也是在接口的实现类中进行增强操作,比如spring的声明式事务管理
加日志也是采用AOP思想,如SpringMVC中要在所有的controller执行前加一行日志,则使用@modelatrrbite注解。本质也是动态代理,cglib的动态代理,所有的controller继承一个父类controller,在父类的某个方法上添加@modelatrrbite注解
实质是cglib生成的子类对象运行一行日志

动态代理
java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理:
具体实现步骤:
实现InvocationHandler 接口,重写invoke方法,定义一个获取代理对象的方法,返回一个代理对象实例Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), this);
第一个类加载器,第二个该接口的所有实现类
cglib动态代理是代理对象类的class文件加载进来,通过修改其字节码生成子类来处理
实现MethodInterceptor接口,intercept方法中实现代理方法,定义一个获取代理对象的方法该方法中要设置父类,因为Cglib是针对指定的类生成一个子类,所以需要指定父类

spring的事务:
PlatformTransactionManager接口
实现类DataSourceTransactionManager ,面向ibatis或者spring jdbc
事务的原子性,一致性,隔离性,永久性
事务的传播机制:
requierd:存在事务,使用事务,无事务,则新建一个事务
supports:支持当前存在事务,使用事务,无事务,则无事务执行
mandatory:使用当前事务,无则抛异常
编程式事务管理
声明式事务管理:
基于xml配置文件方式:配置事务管理器,配置事务的增强,配置切入点和切面
基于注解方式:xml配置事务管理器,开启事务注解,在具体使用事务的方法所在的类上面添加注解:@Transactional

数据库

数据库事务隔离级别:
脏读:脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。
不可重复读:不可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据出现不一致的情况。事务 A 多次读取同一数据,但事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
幻读:一个事务读取的数据可能因为另外一个事务的影响导致不一样,见于多个数据操作
序列化不可重复读,幻读,序列化
MySQL默认的事务隔离级别有四个,分别是Read Uncommitted(读取未提交)、Read Committed(读取已提交)、Repeatable Read(可重复读)和Serializable(串行化)。下面将分别对每个隔离级别进行详细的介绍和解释。

  1. Read Uncommitted:是指在该隔离级别下,事务可以读取其他事务未提交的数据。这种隔离级别最低,会导致脏读(Dirty Read)的问题。脏读是指一个事务在读取到另一个未提交事务的数据后,如果该事务回滚,那么读取到的数据就是无效的。

  2. Read Committed:是指在该隔离级别下,事务只能读取到已经提交的数据。这种隔离级别可以解决脏读的问题,但会引发不可重复读(Non-repeatable Read)的问题。不可重复读是指在一个事务内,多次读取同一数据时,读取结果不一致。

  3. Repeatable Read:是指在该隔离级别下,事务可以重复读取同一数据,读取到的结果始终保持一致。这种隔离级别可以解决不可重复读的问题,但会引发幻读(Phantom Read)的问题。幻读是指在一个事务内,多次执行同一查询,结果集中新增了符合条件的数据。

  4. Serializable:是指在该隔离级别下,事务被严格地顺序执行,不允许并发执行。这种隔离级别可以解决幻读的问题,但会影响系统的并发性能。

sql调优:
1)查询慢日志,筛选sql语句进行优化
2)EXPLAIN查看SQL执行计划:

type列,该列的值表示连接类型。性能顺序system > const > eq_ref > ref > range > index > ALL,杜绝出现all级别。
key列,使用到的索引名。如果没有选择索引,值是NULL。
key_len列,索引长度。不损失精确性的情况下,长度越短越好
rows列,扫描行数。该值是个预估值。
extra列,详细说明。注意,常见的不太友好的值,如下:Using filesort,Using temporary。
1.创建索引:尽量选择纬度高的字段,根据业务合理选择字段创建索引,避免多次回表查询(聚簇索引与二级索引),同时插入更新删除操作会导致索引更新,应该选择合理的字段进行创建
2.避免在索引上进行计算
3.使用预编译查询
4.调整where字句的连接顺序
5.对于复杂的SQL语句,可以考虑将其拆分成多个较小的SQL语句,从而减少单个SQL语句的执行时间
6.用where替换having,因为HAVING会在检索出所有记录之后对结果集进行过滤,而where则是在聚合前筛选记录
7.使用别名
8.合理使用in和exists:IN适合于外表大而内表小的情况;EXISTS适合于外表小而内表大的情况。
9.避免在where子句中对字段进行null值判断,对于null的判断会导致引擎放弃使用索引而进行全表扫描。
10.避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描
11.避免隐式类型转换
12.尽量用union all代替union,union会使用数据库资源进行排序和去重
13.索引数量不要过多,每一次的插入更新删除操作会导致索引更新
14.like查询时避免使用‘%like’,索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较,如果是后缀的%,则不知道是从哪个索引值开始比较,所以会失效
15.避免使用slect *
16.组合索引使用时要注意最左匹配原则
17.使用or关键字进行条件查询时会索引失效
18.索引使用函数会导致索引失效,但是在8.0之后索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引

Spring管理事务默认回滚的异常是什么? RuntimeException或者Error。

mybatis逆向工程:
由数据库的表生成java代码
表到类

springMVC工作原理:
用户发送请求到DispatcherServlet,DispatcherServlet调用HandlerMapping映射为HandlerExecutionChain对象,该对象经过HandlerAdapter获取相关的handler即controller并执行,返回一个ModelAndView,ModelAndView经过DispatcherServlet去调用ViewResolver解析成相关的Model和View,再进行渲染,然后返回给客户端

冒泡排序:当前变量和后面的变量做对比,若大则往后放置一位。temp = a[j-1];a[j-1] = a[j];a[j] = temp

mybtis缓存:
mybtis一级缓存作用范围为同一个sqlseesion,即使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL。
MyBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的,要求实现Serializable接口
二级缓存失效条件:第一次查询未提交,更新数据也会刷新缓存,尽量避免

负载均衡
部署模式:路由模式,桥接模式,服务直接返回模式
算法:轮询,随机,最小连接数(一个节点收到一个任务后连接数就会加1,当节点故障时就将节点权值设置为0,不再给节点分配任务)

mybatis一对多,一对一,多对一
一对一:pojo添加关系,mapper文件中使用resultmap做结果映射,使用association关联
多对一:resultMap为一的一方, collection 对应多的一方(ofType指定pojo)
一对多:resultMap为多的一方,association对应一的一方(javaType指定一的pojo)

redis:
Incr命令:对 key 的值做加加操作并返回
Decr命令:对key的值做减减操作
Append:对指定key的value做追加操作
Expire命令:设置过期时间
SETNX命令:将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。
使用过Redis分布式锁么,它是怎么实现的?
使用 Redis 自带的 SETNX 命令实现分布式锁,先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功,
为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间释放锁,使用DEL命令将锁数据删除

如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?

set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!

使用过Redis做异步队列么,你是怎么用的?有什么缺点?

一般使用list结构作为队列,rpush生产消息,lpop消费消息。

缺点:

在消费者下线的情况下,生产的消息会丢失。

能不能生产一次消费多次呢?

使用pub/sub主题订阅者模式,可以实现1:N的消息队列
缓存穿透:按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。
如何避免?
1:在缓存查不到,DB中也没有的情况,可以将对应的key的value写为null,或者其他特殊值写入缓存,同时将过期失效时间设置短一点,以免影响正常情况。或者该key对应的数据insert了之后清理缓存。
2:在接口层增加校验,不合法的参数直接返回。

缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。
如何避免?
1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
3:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

#{}和${}的区别:
#{}表示一个占位符号,通过#{}可以实现preparedStatement向占位符中设置值,自动
进行java类型和jdbc类型转换。
#{}可以有效防止sql注入。
#{}可以接收简单类型值或pojo属性值。 如果parameterType传输单个简单类型值,#{}括号中可以是value或其它名称。
表示拼接 s q l 串,通过 {}表示拼接sql串,通过 表示拼接sql串,通过{}可以将parameterType 传入的内容拼接在sql中且不进行jdbc类型转换, 可以接收简单类型值或 p o j o 属性值,如果 p a r a m e t e r T y p e 传输单个简单类型值, {}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值, 可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,{}括号中只能是value。

查看源码
jDK8新特性

activemq:
解耦,异步,削峰
消息丢失:
p2p消息默认会保存到activemq服务端知道有消费者将其消费。
topic模式,每个订阅端定义一个 id,<property name=“clientId” 在订阅时向 activemq 注册。
发布消息和接收消息时需要配置发送模式为持久化template.setDeliveryMode(DeliveryMode.PERSISTENT);。
此时如果客户端接收不到消息, 消息会持久化到服务端(就是硬盘上), 直到客户端正常接收后为止。
消息重复问题:
一般来说我们可以在业务段加一张表,用来存放消息是否执行成功,每次业务事物commit之后,告知服务端,已经处理过该消息,这样即使消息重发了,也不会导致重复处理
业务端的表记录已经处理消息的id,每次一个消息进来之前先判断该消息是否执行过,如果执行过就放弃,如果没有执行就开始执行消息,消息执行完成之后存入这个消息的id

分布式事务

  1. 2PC:两阶段提交
    准备阶段,协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复;参与者执行本地事务操作,记录事务日志,但不提交事务;参与者将执行后的信息返回给协调者
    提交阶段,协调者根据反馈情况通知各个参与者commit提交或者rollback回滚,同时参与者将事务提交后的信息返回给协调者,即发送ack消息
    缺点:在提交阶段如果发生异常,会导致部分参与者失败,造成数据不一致,同时也会造成事务信息丢失;性能问题

  2. 3PC:三阶段提交
    相比2PC,在协调者和参与者中都引入超时机制,在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
    准备阶段:协调者向参与者发送准备请求,询问是否可以提交事务,收到信息后进入第二阶段
    第二阶段:协调者根据参与者的反应情况来决定是否可以进行事务的 PreCommit 操作,参与者记录日志,但不提交事务,发送ack消息
    提交阶段:协调接收到所有参与者发送的ACK响应,如果都成功,从预提交状态进入到提交状态,并向所有参与者发送 doCommit 请求,本地事务执行,发送ack给协调者,协调者收到所有ack消息,事务执行完成
    缺点: 数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者因为网络问题无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

  3. 本地消息表
    分为事务主动方和事务被动方,将分布式事务拆分成本地事务进行处理,事务主动发起方需要额外新建事务消息表,并在本地事务中完成业务处理和记录事务消息,并轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
    事务主动方在同一个本地事务中处理业务和写消息表操作
    事务主动方通过消息中间件,通知事务被动方处理事务消息。事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
    务被动方通过消息中间件,通知事务主动方事务已处理的消息。
    事务主动方接收中间件的消息,更新消息表的状态为已处理。
    优点:消息数据的可靠性不依赖于消息中间件,依赖消息表
    缺点:耦合性强;性能受数据库并发性能影响

  4. MQ事务消息
    整体流程与本地消息表一致,唯一不同的就是将本地消息表存在了MQ内部,而不是业务数据库中
    在这里插入图片描述
    主动方向MQ发送消息,MQ将消息持久化,并返回ack给主动方,主动方执行事务,向MQ第二次提交,如commit和rollback,MQ根据第二次提交的信息进行判断,选择是否删除消息

装饰者模式

用于扩展一个类的功能或者给一个类添加附加职责。
动态的给一个对象添加功能,这些功能同样也可以再动态的撤销。
例如给发布的功能接口再动态的添加审核、通知、校验等功能
1.定义一个操作接口

// 接口,定义操作
public interface Component {
    void operation();
}

2.定义原始的被装饰者类,需要实现上述的操作接口

// 原始对象,被装饰者类
public class ConcreteComponent implements Component{
    @Override
    public void operation() {
        System.out.println("拍照");
    }
}

3.定义抽象的扩展综合类,需要实现上述的操作接口

// 抽象扩展综合类,如发布成功后进行的通知、审核、同步其他数据操作等
public abstract class Decorator implements Component {
    // 该接口对象实现了多个对象,Decorator和原始对象ConcreteComponent
    Component component;

    //    将传入的对象直接赋值给当前类的成员变量
    public Decorator(Component component) {
        this.component = component;
    }
}

4.定义一个装饰的方法,例如对上述对象装饰一个通知功能

// 装饰者对象1,实现通知功能
public class ConcreteDecorator1 extends Decorator{
    // 调用父类构造方法,即抽象类的构造方法,将传入的对象直接赋值给当前类的成员变量
    public ConcreteDecorator1(Component component) {
        super(component);
    }
    @Override
    public void operation() {
        // 抽象扩展综合类的方法,即原始对象的方法
        component.operation();
        System.out.println("发布成功后进行通知");
    }
}

5.定义一个装饰的方法,例如对上述对象装饰一个审核功能

// 装饰者对象2,实现审核功能
public class ConcreteDecorator2 extends Decorator{
    // 调用父类构造方法,即抽象类的构造方法,将传入的对象直接赋值给当前类的成员变量
    public ConcreteDecorator2(Component component) {
        super(component);
    }
    @Override
    public void operation() {
        // 抽象扩展综合类的方法,即原始对象的方法
        component.operation();
        System.out.println("发布成功后进行审核");

    }
}

6.测试方法

public class ComponentTest {
    public static void main(String[] args) {
        ConcreteDecorator1 concreteDecorator1 = new ConcreteDecorator1(new ConcreteComponent());
        concreteDecorator1.operation();
        // 拍照
		// 发布成功后进行通知
        System.out.println();
        ConcreteDecorator2 concreteDecorator2 = new ConcreteDecorator2(new ConcreteComponent());
        concreteDecorator2.operation();
        //拍照
		//发布成功后进行审核
        System.out.println();
        // 此处传入了已经被装饰过的对象,即可实现装饰功能叠加 new ConcreteDecorator2(concreteDecorator1);
        ConcreteDecorator2 concreteDecorator3 = new ConcreteDecorator2(concreteDecorator1);
        concreteDecorator3.operation();
        //拍照
		//发布成功后进行通知
		//发布成功后进行审核
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值