两个都是基于List,但是ArrayList的数据结构是数组,LinkedList是链表。其实就是数组和链表的区别。
ArrayList的底层是由一个Object[]数组构成的,而这个Object[]数组,默认的长度是10。
同时LinkedList还实现了Deque接口,可以快速操作头尾元素。
在1.7中,HashMap是使用数组+链表实现的,1.8又加入了红黑树。
数据+链表就是先是长度为16的数组,每个数组的内部有一个链表,链表内容是KeyValue。
在1.8中,链表超出长度后会变成红黑树。
大体流程:
1.根据传入的key值,通过哈希算法和与运算得到数组下标。
2.如果对应下标的元素为空,则将key,value封装成对象(1.7entry键值,1.8node结点)放入该位置
3.如果下标元素不为空,说明数组中已经有一个以上的对象了。
在1.7中,这时会在与数组中对象的key进行比较,如果key相等,则会覆盖之前的value。如果不相等,则要判断需不需要扩容。如果不扩容,使用头插法插入到链表中。
在1.8中,则会先判断这个数组中是链表还是红黑树。如果是链表,因为是尾插法,他会顺便遍历链表,查看是否有重复的key,如果没有,就放到链表尾部,如果这时链表长度超出了8,链表则会转换成红黑树。如果数组里面已经是红黑树了,会根据key进行遍历并添加到红黑树中。node插入完毕后,会判断是否需要扩容,如果不需要,就结束put,需要就扩容。
1.HM非线程同步,HT线程同步。
2.HM允许空值,HT不允许。
3.HM和HT遍历都可以使用Iterator,HT另外可以使用Enumeration。
4.HM数组默认大小是16,增长为2的指数倍。HT默认11,增长为old*2+1。
5.HM继承自AbstractMap类,HT继承自Dictionary类。HM和HT都实现了MAP接口。
==对比的是栈中的值,如果是基本数据类型,比较的就是具体值,如果是引用类型,比较的是堆中对象的地址。
equals如果没有重写,效果和= =一样,String中已经重写过了。
final可以修饰类(无法被继承)、方法(不能被重写override,可以被重载overload)、变量(值不能被修改)
变量分为类变量、成员变量、局部变量
类变量:static修饰的变量,又称为静态变量,随着类加载而生成。存储在方法区中的静态区。如果final修饰,需要在声明时赋值或者代码块中赋值。
成员变量:又称为实例变量,随着对象创建而生成。存储在堆内存的对象中。如果final修饰,需要在声明时赋值或者或者代码块中或者构造器中赋值。
局部变量:需要在使用前赋值。
如果修饰的是基本数据类型,则值不能更改,如果修饰的是引用数据类型,则值可以改,但是引用地址不能改。
局部内部类和匿名内部类只能访问final对象,如果代码中没有写,编译时会自动加上。
为什么呢,因为外部类在运行完成后,可能就直接销毁了,而内部类可能传参引用了外部类的对象,如果外部类销毁了,引用对象也就销毁了,使用时就会报错。所以需要加上final,让参数传入时,copy一个对象当做类的成员变量,之后再引用这个对象,其实引用的是自己的成员变量。同时,内部的成员变量和外部的局部变量,他们在堆中的地址是一致的,所以内部改了值,外面也会一起更改。
Java提供了三种字符串操作类,分别是String、StringBuffer、StringBuilder。这三个类都是java lang包中定义的,都是被final修饰,无法被继承,另外他们都实现了CharSequeue(char s q)接口。
而他们之前的区别在于:String是单一实例,他的字符串数组无法改变,每次去创建或修改String,其实修改的是String对象的地址值,都是创建了新对象,当有大量字符串创建或拼接时,效率十分低下。
于是有了StringBuilder,他的字符串数组可以改变,修改不需要创建新对象,所以效率比String高。
但是StringBuilder不是同步操作的,所以他的线程是不安全的,于是有了StringBuffer,他和StringBuilder差不多,但是每次操作都需要获得锁来保证线程安全,因此效率要比StringBuilder低,但是还是比String高。
1.lambda表达式,本质是内部类
2.函数式接口,只定义了一个抽象方法的接口
3.方法与构造函数的引用
4.streamAPI(创建steam,链式操作,终止操作)
5.并行流和串行流
6.dataAPI更新
7.重复注解和类型注解
8.default和static关键字,接口的默认方法和静态方法
一些小的改动:hashmap,永久代变元空间(方法区),多了抢占线程池。
String转化成StringBuilder
1.直接调用reverse()方法。
2.for循环,StringBuilder.append(String.charAt(i))
以下方法都是String转化成字符数组
3.使用for循环,从尾部开始拼接字符串。
4.使用中间开始,左右互换字符。
5.使用栈,全部弹入,全部弹出。
深拷贝:复制当前值的同时新建一个对象。浅拷贝:复制值的时候也复制地址,两个对象指向同一个地址
1.ThreadLocal是Java提供的线程本地存储机制,他将数据缓存到线程中,线程可以随时获取缓存数据。ThreadLocal对象是公共的,但是里面的值是线程私有的。
2.ThreadLocal是通过ThreadLocalMap来实现的,每一个Thread对象都存放在ThreadLocalMap中,他的key为ThreadLocal对象,value就是里面存放的值。
3.ThreadLocal的内存泄漏问题,首先ThreadLocalMap是被线程调用的,也就是如果线程没有结束,但是方法已经结束,不会在调用到那个ThreadLocal对象时,这个对象并不会被GC清理。这时需要我们自己手动清理对象,调用那个ThreadLocal对象的remove方法即可。
当阻塞队列存在任务时,会持续唤醒核心线程,提高复用率。
创建新线程需要获取全局锁,会影响整体效率,后面线程不需要用的时候,额外线程优先被销毁。
线程的数量也不是越多,任务处理速度就越快的,因为其实线程切换也需要消耗时间,频繁切换,影响整体效率。创建和销毁线程需要资源和时间就更不用说的。所以控制一个合理的线程数量是最好的。
1.通过继承Thread类,并且重写该类的run()方法,run()就是线程要完成的任务。然后再创建Thread子类的实例,这就是线程,start()启动。
2.通过实现Runnable接口重写run()方法,run()方法代表线程要执行的任务。将子类作为target生成线程。
3.通过callable和future创建。创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。使用FutureTask对象作为Thread对象的target创建并启动新线程。调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
4.通过线程池创建线程
以上4种方法本质都是实现Runnable接口
Spring是一款开源框架,包含了IOC、AOP和DI。
控制反转(IOC),传统的 java 开发模式中,当需要一个对象时,我们会自己使用 new 或者 getInstance 等直接或者间接调用构造方法创建一个对象。而在 spring 开发模式中,spring 容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用 spring 提供的对象就可以了,这是控制反转的思想。
依赖注入(DI),spring 使用 javaBean 对象的 set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。
面向切面编程(AOP),在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等
公用操作处理的过程就是面向切面编程的思想。AOP 底层是动态代理,如果是接口采用 JDK 动态代理,如果是类采用CGLIB 方式实现动态代理。
SpringMVC是基于Spring的一个web框架,和spring、mybatis组合使用就有了ssm框架,可以解决很多的web开发需求。
而SpringBoot是基于Spring开发的一款轻量级web框架。
SpringBoot和Spring具体的区别有以下几点:
1.SpringBoot可以开发单独Spring应用。
2.内置了Tomcat之类的容器,无需部署。
3.不需要配置复杂的xml文件
4.自动配置spring,bean注入改为使用注解注入的方式(@Autowire)
5.提供了一些第三方工具,比如表单验证
6.整合了常用依赖,引入springcore,boot会自动引入其他依赖。
2、初始化Spring上下文:初始化Spring上下文,并将Spring配置信息注入Spring上下文;
3、初始化Spring容器:初始化Spring容器,将Spring上下文注入Spring容器,并完成Spring容器的初始化;
4、加载应用程序:加载应用程序,将应用程序的配置信息注入Spring容器;
5、启动Spring容器:启动Spring容器,完成Spring容器的初始化,并将应用程序的配置信息注入Spring容器;
6、启动应用程序:启动应用程序,完成应用程序的启动,并开始处理请求。
1.客户端发送请求,请求传到Servlet
2.Servlet根据url找到对应的处理器,Controller
3.找到以后调用处理器的方法,处理后的结果会封装到ModelAndView对象中
4.servlet解析这个对象,获得JSP页面
5.JSP页面返回客户端
实例化(实例化bean对象,并调用其构造函数)-属性赋值(设置bean对象的属性)-初始化(调用bean对象的初始化方法)-生存期(使用bean对象)-销毁(调用销毁方法)
1.Spring IOC容器可以管理bean的生命周期,Spring允许在bean生命周期内特定的时间点执行指定的任务。
2.Spring IOC容器对bean的生命周期进行管理的过程:
① 通过构造器或工厂方法创建bean实例
② 为bean的属性设置值和对其他bean的引用
③ 调用bean的初始化方法
④ bean可以使用了
⑤ 当容器关闭时,调用bean的销毁方法
3.在配置bean时,通过init-method和destroy-method 属性为bean指定初始化和销毁方法
4.bean的后置处理器
① bean后置处理器允许在调用初始化方法前后对bean进行额外的处理
② bean后置处理器对IOC容器里的所有bean实例逐一处理,而非单一实例。其典型应用是:检查bean属性的正确性或根据特定的标准更改bean的属性。
③ bean后置处理器时需要实现接口:
org.springframework.beans.factory.config.BeanPostProcessor。在初始化方法被调用前后,Spring将把每个bean实例分别传递给上述接口的以下两个方法:
postProcessBeforeInitialization(Object, String)
postProcessAfterInitialization(Object, String)
5.添加bean后置处理器后bean的生命周期
①通过构造器或工厂方法创建bean实例
②为bean的属性设置值和对其他bean的引用
③将bean实例传递给bean后置处理器的postProcessBeforeInitialization()方法
④调用bean的初始化方法
⑤将bean实例传递给bean后置处理器的postProcessAfterInitialization()方法
⑥bean可以使用了
⑦当容器关闭时调用bean的销毁方法
首先spring事务有acid四大特性,和数据库一致。
事务的隔离级别也有四种,和数据库一致。
事务的传播行为有七种,requierd(默认,有点像单例,如果没有,创建事务,如果有,就用这个),supports(使用当前事务,如果当前没有事务,就不按事务走),mandatory(使用当前事务,如果没有,就抛出异常),required_new(新建事务,如果已有,前面的挂起),not_supported(非事务,如果已有,前面的挂起),never(非事务,如果有抛出异常),nested(如果没有新建,如果有则嵌套)
事务的四种实现方式,编程式事务管理,基于 TransactionProxyFactoryBean的声明式事务管理,基于 @Transactional 的声明式事务管理,基于Aspectj AOP配置事务
相同:
1.都是spring提供的IOC容器。他们都是java的接口,而ApplicationContext是继承自BeanFactory(BeanFactory—ListableBeanFactory—ApplicationContext)。
2.都可以配置XML属性,支持属性的自动注入。
3.都提供了一种方法,使用getBean获取Bean。
不同:
1.调用getBean,BF会实例化对象,而AC会在启动容器时就实例化对象。
2.BF不支持国际化,即i18n(语言切换),AC提供了支持。只在国内使用,BF无所谓,国际使用,需要用AC。
3.AC支持监听器的Bean。
4.BF的核心实现是XMLBeanFactory,而AC的核心实现是ClassPathXMLApplicationContext。
5.如果是使用自动注入,BF需要使用AutoWired注解,AC则是XML进行配置。
总的来说,AC是BF的子类,BF提供了基本的IOC和DI功能,AC拥有比BF更全面方便的功能,具体使用应该是AC优于BF。
堆,方法区是共享的,栈、本地方法区、程序计数器是线程独享的。
栈中的本地变量、方法区中的静态变量、本地方法栈中的变量、正在运行的线程等可以作为gc root。
什么是gc root?JVM在进行垃圾回收时,需要找到垃圾对象,但是垃圾对象难找,所以思路是找到非垃圾对象,最好的方法是先找到”根“对象。这些”根“对象有一个特征,就是它只引用其他对象,而不会被其他对象引用,所以称为根,然后就是顺着根一直往下找还在被使用的对象,标记成非垃圾。
JVM使用类加载系统加载class文件,将数据放到运行数据区中,然后用字节码引擎去运行代码。类会生成线程栈,同时会在程序计数器区中生成一个来给自己用,而类中的方法放在栈帧中,栈帧中分成了四个部分:栈堆中局部变量表,用来计算的操作数栈,指向方法区的动态链接,指向方法返回位置的方法出口。
主要是调整jvm的一些参数,设置一些跟内存、垃圾回收相关的参数,不过这个一般默认的够用。
如果垃圾回收器在最小、最大之间收缩堆而产生额外的时间,我们就把最大、最小设置为相同的值
还有就是设置新生代和老年代的内存占比,默认是1:2,但是还是要根据实际项目的具体情况去分析。
更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC,因为老年代满了就会fullgc嘛。
更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
如何选择应该依赖应用程序对象生命周期的分布情况:如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。
调优的最终目的就是gc时间少,gc次数少,fullgc的周期也要延长。
java命令生成dump文件,然后用vm去分析dump文件。
每种类型都有自己的优势与劣势。重要的是,我们编程的时候可以通过JVM选择垃圾回收器类型。我们通过向JVM传递参数进行选择。每种类型在很大程度上有 所不同并且可以为我们提供完全不同的应用程序性能。理解每种类型的垃圾回收器并且根据应用程序选择进行正确的选择是非常重要的。
1、串行垃圾回收器
串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。
通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
2、并行垃圾回收器
并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,它也会冻结所有的应用程序线程当执行垃圾回收的时候
3、并发标记扫描垃圾回收器
并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
当标记的引用对象在tenured区域;
在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。
通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
4、G1垃圾回收器
G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域
通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器
1.用jmap查看jvm区域使用情况
2.通过jstack来查看线程运行情况
3.通过jstat来查看垃圾回收的情况,特别是频繁地fullgc,就得调优了
4.找到占用cpu最多的线程,定位到具体方法,优化方法运行
Redis所有数据的结构都是KV结构,只是V的数据接口会有所不同,主要分以下五个:
1.字符串:最简单的数据缓存。可以实现分布式锁、计数器、session共享、分布式ID
2.哈希表:存储KV,主要用来存储对象
3.列表:一般是栈或者队列,可以缓存微信公众号、微博等消息流数据
4.集合:和列表类似,但是里面的元素不能重复。集合可以进行交集并集差集等操作,可以实现共同关注、朋友圈点赞等操作
5.有序集合:可以设置顺序,可以用来实现排行榜功能
1.最基础的是主从模式,主库进行读写,从库进行数据同步。但是在这种模式下,如果主库或从库宕机,需要手动修改IP,而且也很难扩容,不怎么使用。
2.哨兵(sentinel)模式,哨兵模式就是在主从模式上加入了哨兵节点,哨兵发现主库宕机后,会从库中选择一个库作为新的主库。同时哨兵也可以做集群策略,一个哨兵节点宕机后,会有其他的结点继续工作。
哨兵模式原理:当哨兵启动后,他会每秒向所有监管的库发出PING,如果一个实例距离最近一次有效PING返回时间超出了设定值,这个库在哨兵眼里就进入了主观下线转态。当有足够数量的哨兵都认为该库主观下线,那该库就会进入客观下线,所有的哨兵会进行投票,选取一个从库称为主库。同时发布订阅模式,将所有的从库更换主库。
3.Cluster模式,多主多从模式,数据会根据Key进行槽位分配,不同的槽会分配到不同的主库中。提高了Redis的容量,并且可以持续扩展。
缓存穿透就是用户高频率恶意地查询一条找不到的数据,Redis找不到数据后,就会穿透到MySQL中,使MySQL承受压力很大。
解决方案:
1.布隆过滤器,将所有可能查询的参数用hash形式存储,在控制层先校验,不符合的就丢弃,直接避免了查询数据库。匹配的key不一定存在,不匹配的key一定不存在。
2.在redis中储存一个空对象,同时也需要设置失效时间。配合布隆过滤器,效果最佳。这个有两个问题:缓存会占用空间、缓存层和存储层数据可能会不一致。
缓存击穿就是热点数据有大量访问,但是如果这个热点数据过期了,大量请求同一时间直接请求到了存储层,就像是Redis被击穿了一样。
解决方案:
1.热点数据永不过期
2.加上互斥锁,同一个key只有一个线程可以直接访问存储层。
3.二级缓存
雪崩就是当数据量级很高时,把服务器宕机了,数据会堆积,越来越多,而服务器重启后又会宕机,形成数据雪崩。或者是大量数据同时失效导致服务器宕机。
解决方案:
1.redis高可用,搭建redis的集群
2.限流降级,缓存失效后,通过锁或者队列来控制读写数据库缓存的数量。
3.数据预热,在正式部署前,先访问一遍数据,在即将发生大并发访问前手动触发加载缓存不同的key。设置缓存失效时间不同。
4.二级缓存
推荐使用MySQL锁和redis原子操作
在redis中根据商品id,创建
当大量请求并发时,首先先去redis中查询商品对应的key“product_id”,如果没有返回值,说明redis中没有这条数据。调用“mysql.query(“select amount from store where product_id = {id}”)”查询到商品数量,然后调用“jedis.setnx(“product_” + id, number)”将数据存入redis,顺便再加上过期时间。
获得商品数量后,和订单需求量进行比较,库存不够直接返回。
下一步更改redis中的数据,减去订单需求量,获得返回值,即现库存,再次进行比较,返回值是否大于0。如果大于等于0,则认为购买成功,修改MySQL数据。如果小于0,说明数量不够或者其他线程已经买走,则把刚才减的值加回去。
redis持久化有两种方式,RDB(定时将内存中的数据dump到磁盘上)和AOF(将redis的操作日志以追加的方式写入文件,每次缓存操作都会记录)
1.定期更新:定期更新Redis中的数据,可以通过定时任务或者定期抓取数据库中的数据,将最新的数据放入Redis中
2.监控热点数据:可以通过分析数据库中的数据,监控热点数据,将热点数据放入Redis中
3.缓存策略:可以采用LRU(Least Recently Used)最少最近使用缓存策略,将最近使用次数最多的数据放入Redis中
Redis是一个开源的内存数据库,它的基本数据类型包括:
确定架构-安装-配置-启动-构建集群-测试:
1、确定缓存集群的架构:确定缓存集群的架构是根据实际业务需求来决定的,比如是单机模式,还是主从模式,还是哨兵模式,还是集群模式等。
2、安装Redis:安装Redis,可以通过源码安装或者是通过包管理器安装,比如yum或者apt-get等。
3、配置Redis:根据确定的架构,配置Redis,比如修改Redis配置文件,设置Redis密码、主从复制、哨兵监控等。
4、启动Redis:启动Redis,启动Redis服务,检查Redis服务是否正常运行。
5、构建集群:构建Redis集群,使用Redis的cluster命令,将多台Redis服务器构建成一个集群。
6、测试集群:测试Redis集群,检查集群是否正常运行,检查集群的数据是否正确。
哨兵机制是一种高可用的Redis集群解决方案,它可以自动检测主节点的故障,并将备用节点升级为主节点,以确保Redis集群的高可用性。
保持Redis和数据库的一致性有很多种方法,比如:
1.没有使用最左前缀
2.大于号右边
3.前置模糊的like搜索
4.or连接的两个条件中有一个没有索引
5.where中索引列有运算
6.where用到了聚合函数
7.数据少的时候
8.使用了is not null,!=,不能确定范围的范围查找
9.隐式转换:应该加引号的string没有加
总得来说,就是索引的最左部分是排序的,但是后面是无序的,不管是联合索引(联合索引本质是创建了a,ab,abc索引)还是单列索引都是这样的,如果最左没有用到索引,索引就不会生效。还有就是引擎判断全盘扫描比使用索引更快,通常出现在查询时需要使用全盘和索引时。
脏读:当他人在进行事务时,你读到其他人提交到一半的数据,而后面那个事务回滚,那么你读到的数据就是虚假的。
不可重复读:由于他人在中间修改了数据,导致两次查询数据不同。
幻读:由于他人在中间增减了数据,导致两次查询数据不同。
第一类丢失更新:你的事务回滚覆盖了其他人提交的数据。
第二类丢失更新:你的事务提交覆盖了其他人提交的数据。
数据库的隔离级别从低到高有四个:读未提交,读已提交,可重复读,串行化。
读未提交:没有上锁,用户可以直接读取到其他人未提交的数据。平时不用。解决第一类丢失更新。
读已提交:oracle默认,用户只能读到别人已提交过的数据。解决脏读。
可重复读:MySQL默认,用户只能读到自己事务开始时的数据快照(快照读),修改数据是在最新的快照上修改(当前读)。解决不可重复读、第二类丢失更新、幻读的读问题。
串行化:隔离级别最高,事务以串行的方式排队进行。事务在读写数据时,会对读写的数据加上共享锁(读锁,还有一个叫排他锁,是写锁),其他事务进行需要中读写该数据的事务要等到该数据释放锁才能继续读写数据。解决以上所有问题。
读已提交和可重复读有相似性,他们都使用了快照,不同的是:读已提交中,每次读操作会更新该数据的快照,而可重复读则是在事务开始时会记录一张自己的快照。
原子性:事务中的操作,要么全完成,要么全回滚。
一致性:数据库只包含事务成功提交的数据。一致性保证事情的发展如期望一样,他是最终目的,其他三个是为了成就一致性。
隔离性:并发事务不能相互干扰。我不应该读到别人修改但是未提交的数据
持久性:成功提交的数据不应该被其他故障或操作影响。我提交的数据不应该被别人非正常覆盖或者被故障销毁。
一般是表格设计合理、查看索引是否失效、索引优化(索引不要做函数操作)、sql语句优化(sql语句的优化看隔壁)
乐观锁认为在使用时其他人不会使用这条数据,因此只在更新时才会判断别人是否更新过数据,版本快照等方式。
悲观锁认为使用时别人也会使用,因此在操作时,会独占锁,然后再操作资源。
在Java中RabbitMQ重复消费的问题通常是因为以下原因产生的:
消息未被确认:当消费者消费完消息后未将消息确认给RabbitMQ,RabbitMQ会认为该消息未被消费,会将该消息重新投递给其他消费者进行处理,从而导致消息重复消费。
消息消费异常:当消费者在处理消息时发生异常,RabbitMQ会认为该消息未被消费,会将该消息重新投递给其他消费者进行处理,从而导致消息重复消费。
针对这些问题,可以采取以下措施来解决:
消息确认:在消费完消息后,一定要将消息确认给RabbitMQ,告知它该消息已被消费。可以使用自动确认或手动确认的方式来确认消息,手动确认方式更为可靠。
消息幂等性设计:在消费者处理消息时,需要考虑消息幂等性,即同一消息重复消费时对系统状态不会造成影响。可以采用业务状态判断或唯一id判断等方式来实现消息幂等性设计。
消息去重:在消费者处理消息时,可以采用消息去重的方式来避免消息重复消费。可以在系统中记录已经消费过的消息id,当收到重复消息时,直接忽略该消息。 总之,在使用RabbitMQ时,需要注意消息的确认和幂等性设计,从而避免消息重复消费的问题。同时,也可以采用消息去重的方式来提高系统的可靠性和稳定性。
除了基本的发送和接收消息,还可以使用RabbitMQ提供的其他功能,例如:
1、同步锁
同一时刻,一个同步锁只能被一个线程访问。以对象为依据,通过synchronized关键字来进行同步,实现对竞争资源的互斥访问。
2、独占锁(可重入的互斥锁)
互斥,即在同一时间点,只能被一个线程持有;可重入,即可以被单个线程多次获取。什么意思呢?根据锁的获取机制,它分为“公平锁”和“非公平锁”。Java中通过ReentrantLock实现独占锁,默认为非公平锁。
3、公平锁
是按照通过CLH等待线程按照先来先得的规则,线程依次排队,公平的获取锁,是独占锁的一种。Java中,ReetrantLock中有一个Sync类型的成员变量sync,它的实例为FairSync类型的时候,ReetrantLock为公平锁。设置sync为FairSync类型,只需——Lock lock = new ReetrantLock(true)。
4、非公平锁
是当线程要获取锁时,它会无视CLH等待队列而直接获取锁。ReetrantLock默认为非公平锁,或——Lock lock = new ReetrantLock(false)。
5、共享锁
能被多个线程同时获取、共享的锁。即多个线程都可以获取该锁,对该锁对象进行处理。典型的就是读锁——ReentrantReadWriteLock.ReadLock。即多个线程都可以读它,而且不影响其他线程对它的读,但是大家都不能修改它。CyclicBarrier, CountDownLatch和Semaphore也都是共享锁。
6、读写锁
维护了一对相关的锁,“读取锁”用于只读操作,它是“共享锁”,能同时被多个线程获取。“写入锁”用于写入操作,它是“独占锁”,只能被一个线程锁获取。Java中,读写锁为ReadWriteLock 接口定义,其实现类是ReentrantReadWriteLock,包括内部类ReadLock和WriteLock。方法readLock()、writeLock()分别返回度操作的锁和写操作的锁。
(至于“死锁”,并不是一种锁,而是一种状态,即两个线程互相等待对方释放同步监视器的时候,双方都无法继续进行,造成死锁。)
分布式中,最多满足一致性C、可用性A和分区容错性P三种中的两种。
ca一般是mysql和Oracle,另外两个都是no sql
通过CAP理论,我们知道无法同时满足一致性、可用性和分区容错性这三个特性,那要舍弃哪个呢?
CA without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但其实分区不是你想不想的问题,而是始终会存在,因此CA的系统更多的是允许分区后各子系统依然保持CA。
CP without A:如果不要求A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。
AP wihtout C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。
分布式锁解决的问题是分布在多台机器中的线程对共享资源的互斥访问,实现方式有:
1.基于MySQL中的行锁来达到互斥访问,但是MySQL的加锁和释放锁性能差,一般不使用
2.基于Zookeeper,Zookeeper中数据存在内存中,性能会优于MySQL,并且Zookeeper的顺序节点、临时节点、Watch机制能很好的实现分布式锁。
3.基于Redis,Redis数据也是存在内存中,基于Redis的消费订阅、数据超市、lua脚本等功能,也能很好地实现分布式锁。
IO的类型有五种,按阻塞程度分别是阻塞IO,同步非阻塞IO,多路复用IO,信号驱动IO,异步IO。
阻塞IO在运行时,进程会被阻塞,一直等待内核中的文件准备完成,然后复制到用户空间。
同步非阻塞IO会每隔一段时间向内核发出确认,在等待回复的期间,进程是堵塞的,如果回复的结果是文件准备完成,就将文件复制到用户空间。
多路复用IO用的是selector中的select()对一个文件进行监控,监控期间进程会被阻塞,直到返回了可读条件。本质上和阻塞IO一样,但是selector是同时监控多个文件,类似多线程,所以实际效率很高。
信号驱动IO在启动时,会向内核注册一个回调机制,这个机制去监控文件。如果文件准备完成,回调机制向进程发出信号,进程开始复制文件。
异步IO中进程一开始就调用aio_read并给内核缓冲区指针、缓冲区大小、传递描述符等参数,然后让内核自己去完成文件的准备和复制,全部完成后发送信号告诉进程完成此次IO。