①ArrayList它的底层是数组,有下标的概念,可以通过index下标直接定位元素,所以查询快;在增删时,会进行扩容判断和拷贝,所以增删慢。
②LinkedList的底层是双向链表。每次查询都要循环遍历,所以查询慢;在增删时只需要链接新的元素,而不用修改列表中剩余的元素,所以增删快。
集合有的快有的慢,主要是因为它们的底层实现不同。
有很多种方法,最简单的就是使用StringBuilder类,它里面有个reverse()方法,可以将字符串进行倒序输出。
第二种是String类的toCharArray()方法把字符串转换为char数组。使用循环将数组中的字符进行倒序交换,输出结果。
toCharArray() 将字符串转换为字符数组
split(String regex) 切割
substring(int beginIndex, int endIndex)截取字串
int length() 获取字符串的长度
int indexOf(String str) 获取特定字符的位置
toUpperCase() 转大写转小写忽略大小写
看一段代码:
public class Test {
public static void main(String[] args) {
String a = "abc";
String b = "def";
String c = "abc" + "def";
String d = a + "def";
String e = new String("abc") + "abc";
String g = "abcdef";
}
}
反编译后
public class Test
{
public Test()
{
}
public static void main(String args[])
{
String a = "abc";
String b = "def";
String c = "abcdef";
String d = (new StringBuilder()).append(a).append("def").toString();
String e = (new StringBuilder()).append(new String("abc")).append("abc").toString();
String g = "abcdef";
}
}
结论:字符串+拼接如果无变量和new关键词参与则在字符串常量池。。
如果有变量或者是new关键词参与拼接, 那么就会都new出一个StringBuilder对象, 然后使用append方法, 随后又使用toString方法来new一个对应的String类, 这样繁琐的创建对象, 不仅消耗时间, 还会消耗内存资源。
所以:循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。而不要使用+
说到集合框架我们可以把集合框架分为两个部分,单列集合collection和双列集合顶层接口 map。其中map的存储是key-value形式,collection下面有set接口特点是存储无序不可重复,list接口存储有序可重复。
列表list(集合有序可重复,可有null元素) | 集set(元素是无序不可重复的) | map(顶层接口,存储的是键值对,键是唯一的。) |
---|---|---|
Vector:底层数据结构是数组数据结构.特点是查询和增删速度都很慢。 集合长度。线程安全。 | HashSet:底层数据结构是哈希表、存取速度快、元素唯一、线程不安全。 | HashMap:底层是哈希表数据结构;允许使用null键和null值;线程不安全,效率高; |
ArrayList:底层的数据结构是数组数据结构,特点是查询速度快(因为带下标),但是增删速度稍慢,因为当元素多时,增删一个元素则所有元素的下标都得改变,线程不安全。默认长度是10,当超过长度时,按1.5倍延长集合长度。 | TreeSet:底层数据结构式二叉树。可以对Set集合中的元素进行排序。元素有序、线程不安全。 | HashTable: 底层是哈希表数据结构;不可以使用null键和null值线程安全效率低,因为它里面的方法都是用了 synchronized关键字修饰。 |
LinkedList:底层数据结构式双向链表数据结构(即后面一个元素记录前一个),特点:查询速度慢,因为每个元素只知道前面一个元素,但增删速度快,因为元素再多,增删一个只要让其前后 的元素 重新相连即可,线程不安全。 | ①特点:有序的,保证元素不可重复的前提下,维护了一层添加顺序,判断是否重复的依据:hashCode、equals | TreeMap:底层是二叉树结构;允许使用null键和null值;线程不安全; |
①定义线程安全的Map可以通过在每个方法上添加synchronized关键字实现或者使用Collections.synchronizedMap方法来保证线程安全。但是这样效率比较低。
②还可使用ConcurrentHashMap。原理是采用分段锁的机制,将整个Map分成多个Segment,在每个Segment上都加锁,不同的线程可以同时访问不同的Segment,这样兼顾了线程安全和运行效率。
①==:如果比较的对象是基本数据类型,则比较的是数值;如果比较的是引用数据类型,则比较的是对象的地址值。
②equals():用来比较两个对象的内容是否相等。equals方法不能用于基本数据类型的变量,如果没有对equals方法进行重写,则比较的是对象的地址值。
hashmap底层是哈希表存储是无序的它和HashTable最大的区别是hashmap线程不安全并且key值允许为null,而HashTable线程安全并且key值不允许为null,由于Hashtable直接在put和get方法上面加synchronized关键字来实现线程安全所有操作都需要竞争同一把锁所以效率很低。
区别 | 存储 | 底层 | 如何选择 | key是否允许null | 是否线程同步 |
---|---|---|---|---|---|
HashMap | 存储无序 | 哈希表 | 不需要排序 | 允许 | 非线程安全 |
HashTable | 存储无序 | 哈希表 | 需要线程安全 | 不允许 | 线程安全 |
TreeMap | 存储有序 | 红黑树 | 需要排序 | 不允许 | 非线程安全 |
LinkedHashMap | 存储有序 | 链表和哈希表 | 需要存储有序 | 允许 | 非线程安全 |
public class Test1{
//1、私有化本类所有的构造方法
private Test1(){}
//2、直接在本类中创建唯一对象
private static Test1 t1 = new Test1();
//3、提供外界获取唯一对象的方法(公共的、静态的)
public static Test1 getInstance(){
return t1;
}
}
懒汉式单例设计模式的特点
①懒汉式模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance (获取实例)方法 时才去创建这个单例。(比较懒别人喊一次才动一次?)。
②好处:不存在浪费内存的问题,弊端:在多线程环境下,可能不能保证对象是唯一的
public class Test2{
//1、私有化本类所有的构造方法
private Test2(){ }
//private和static修饰的成员变量
private static Test2 t2;
//3、提供外界获取唯一对象的方法(公共的、静态的)
public static Test2 getInstance(){
if(t2 == null){
//2、在本类中创建唯一对象
t2 = new Test2();
}
return t2;
}
}
哈希表又叫散列表是一种数据结构,特点是查找增删都很快。哈希表里面每个数据都有唯一的键(key),把这个关键码值(key)通过映射函数映射到哈希表中一个位置来访问。
这个映射函数叫做哈希函数(散列函数),可以把任意长度的key值变换输出成固定长度的哈希值(Hash value)。一个好的哈希函数能够将键均匀地映射到哈希表中,以减少冲突和查找时间。
Hash code是一种编码方式,在Java中,每个对象都会有一个hashcode。Java可以通过这个hashcode来识别一个对象。
哈希函数把key变换输出为哈希码(哈希值),当不同的key值产生的哈希值H(key)是一样的,就产生了哈希冲突。
解决哈希冲突的方法
(1) 再哈希法
当发生冲突时,用不同的哈希函数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
(2) 链地址法
通过将具有相同哈希码的数据元素存储在一个链表中,来避免冲突的发生。
在查询、插入或删除数据元素时,首先根据哈希码找到对应的链表,然后在链表中搜索或修改相应的数据元素。
(3)建立公共溢出区
将哈希表分为基本表和溢出表两部分,将冲突的元素存储在另一个溢出表中
final类:不可被继承,如java.lang.Math就是一个 final类,不可被继承。
final方法:不可被重写
final变量:final修饰的变量是一个常量一般和static联用。只能被赋值一次在初始化后不可改变变量值。如果final变量是引用变量,则不可以改变它的引用对象,但可以改变对象的属性。
public static final double pi=3.14;
放在常量的类里,配置到数据库里,放在配置文件里,放到缓存里面,第三方配置中心
Java8出现了很多新特性我说几个比较大的改变,第一个是接口可以写默认方法和静态方法,默认方法用default修饰符标记,不强制实现类实现默认方法。第二个是stream流,提供了便捷快速的操作数据的方式。第三个是Lambda表达式和函数式接口,函数式接口仅有一个抽象方法的接口,Lambda 表达式本质上是一个匿名方法,让我们的代码更简洁直观。第四个Java 8引入了重复注解的概念,允许在同一个地方多次使用同一个注解。
1)饿汉式单例设计模式的特点
①饿汉式模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance (获取实例)方法之前单例就已经存在了。(比较饿,没喊就迫不及待的创建了)。
②好处:就算在多线程环境下,也一定可以保证对象是唯一的。弊端:创建比较早,有浪费内存的现象。
①JVM是Java虚拟机,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言非常重要的特点跨平台性就是通过jvm实现,Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。
整个JVM 框架由加载器加载文件,然后执行器在内存中处理数据,通过本地接口与异构系统交互
②JVM包括
1、类加载器(Class Loader )类加载器的作用是加载类文件到内存
2、执行器(Execution Engine) 执行引擎执行引擎也叫做解释器,负责解释命令,提交操作系统执行。
3、本地接口(Native Interface )作用是融合不同的编程语言为Java 所用
4、运行时数据区(Runtime data area) 我们所有写的程序都被加载到这里之后才开始运行。包括:
(1)线程共享的堆(含有字符串常量池),元空间(元空间在本地内存中,包含了加载的类信息和运行时常量池)
(2)线程私有的虚拟机栈、本地方法栈以及程序计数器
①栈是运行时的单位 , 是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。所有的 Java 方法调用都是通过栈来实现的,方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
②Java 堆是所有线程共享的最大的一块内存区域,唯一目的就是存放对象实例。里面包含字符串常量池(针对字符串专门开辟的一块区域,主要目的是为了避免字符串的重复创建。)。因为Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆。通过垃圾回收算法 Java 堆还可以细分为:新生代和老年代。
③方法区(元空间)
方法区是被所有线程共享,方法区会存储已被虚拟机加载的 类信息、字段信息等。JDK1.8 最大的变化就是方法区的位置转移到了本地内存中被称为元空间。元空间中还包含运行时常量池:各种字面量和符号引用的常量池表 。
④程序计数器
线程私有的,是一块很小的内存空间,每个线程都有一个程序计数器,代码的流程控制,如分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
⑤本地方法栈
本地方法栈为JVM调用native方法时服务,一个Native方法就是一个java调用非java代码的接口。
PS:直接内存在本地内存中,不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现
①垃圾回收器通常是作为一个单独的低级别的线程运行,通过垃圾回收算法对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
②JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。使用标记整理算法主要有初始标记,并发标记,最终标记,筛选回收四个过程。G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率。
①引用计数法
每当有一个地方引用它,计数器就加 1;
当引用失效,计数器就减 1;
任何时候计数器为 0 的对象就是不可能再被使用的。
②可达性分析算法
以 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
引用的概念:
引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
1、强引用:最普遍的引用如String s = new String(“aaa”)。如果一个对象具有强引用,垃圾回收器绝不会回收它
2、软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
3、弱引用:的对象拥有更短暂的生命周期。一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
4、虚引用:虚引用并不会决定对象的生命周期。是一种形同虚设的引用,随时会被回收。
①长期存活的对象将进入老年代
对象首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC (新生代GC)后仍然能够存活年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。
②大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
③新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,也比较快。
老年代 GC(Major GC/Full GC):指发生在老年代的 GC,Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。
①标记清除算法
标记阶段,标记所有的可访问对象。
收集阶段,垃圾收集算法扫描堆并回收所有的未标记对象。
② 标记整理算法
标记-整理(Mark-and-Compact)算法是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
③复制算法
将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除。特点:不会产生空间碎片;内存使用率极低
④分代收集算法
不同的对象的生命周期是不一样的。分代回收把不同生命周期的对象 放在不同代上,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,老年代中因为对象的存活率极高,所以采用标记清理或者标记整理算法进行回收。
1、内存分配:根据应用程序的需求调整JVM的内存分配。如果应用程序需要处理大量数据,则可以增加JVM的堆内存和非堆内存。
2、垃圾回收:JVM的垃圾回收是一项重要的任务,它可以释放无用的对象并回收内存。通过选择不同的垃圾回收器、调整垃圾回收器的参数和设置合适的内存阈值等,可以提高垃圾回收的效率。
3、线程管理:JVM中的线程是Java应用程序的核心组成部分。通过调整线程的数量、设置线程的优先级和使用线程池等方式,可以提高Java应用程序的并发性能。
4、类加载:Java应用程序需要在运行时动态加载类。通过优化类的加载过程,可以提高应用程序的启动速度和响应性能。
5、编译优化:JIT编译器可以将Java代码编译为本机代码,从而提高应用程序的执行速度。通过设置JIT编译器的参数和选择适当的编译器,可以提高编译器的性能和效率。
6、I/O优化:I/O操作是Java应用程序中常见的性能瓶颈之一。通过使用缓冲区、选择合适的I/O库、减少I/O操作次数等方式,可以提高I/O操作的效率。
JVM中类的装载是由类加载器和它的子类来实现的,类加载器负责在运行时查找和装入类文件中的类。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。
1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
2)如果类中存在初始化语句,就依次执行这些初始化语句。
①主内存 :所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)
②本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。
1)创建类的实例,也就是 new 一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4) 反 射 (Class.forName(“com.lyj.load”))
5)初始化一个类的子类(会首先初始化子类的父类)
6)JVM 启动时标明的启动类,即文件名和类名相同的那个类只有这 6 中情况才会导致类的类的初始化。
类的初始化步骤:
1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3)加入类中存在初始化语句(如 static 变量和 static 块),那就依次执行这些初始化语句。
①读未提交 : 可以看到其他事务没有提交的结果。等于没有任何隔离性。
②读已提交 :只能看到其他事务已经提交的改变。这种隔离级别会引起不可重复读。
③可重复读: 它保证了可重复读取同样的数据,即同一个事务多次读取操作数据会看到同样的数据。解决了不可重复读但可能造成幻读。
④可串行化 : 通过强制事务排序,解决了幻读的问题。它在每个读的数据行上面加上共享锁。(实际中基本不使用)
不同隔离级别可能导致的问题
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
可串行化 | 不可能 | 不可能 | 不可能 |
①脏读:当一个事务读取到另外一个事务修改但未提交的数据时,就可能发生脏读。
②不可重复读:就是说,比如在A事务中进行多次相同的查询,B事务在A事务多次查询之间修改对应表中的数据,导致A事务多次读取的结果不一致。
③幻读是’‘不可重复读’'的一种特殊场景:例子:事务一将表中性别列的值都更改为1,事务二又添加了一条"性别”的值为0记录,事务一再查询所有的记录时会发现有多了一条记录的“性别为0,这种情况就是所谓的幻读
不可重复读和幻读区别
不可重复读:一个事务中多次读取同一数据,但是由于其他事务的修改,导致两次读取的结果不一致。
幻读:一个事务中多次读取同一范围内的数据,但是由于其他事务的插入操作,导致两次读取的结果不一致。
①原子性
事务要么全部提交成功,要么全部失败回滚,不能分割执行其中的操作。
②一致性
事务的执行不会破坏数据关系的完整性和业务逻辑的完整性。
③隔离性
一个事务不会被另一个事务影响,第一个事务执行完成后再执行另一个事务,但处于性能上的考虑,一般都需要事务并发执行,所以隔离程度有区别。
④持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
批处理(batch) 操作数据库,批处理指的是一次操作中执行多条SQL语句。
第一步:打开mysql的批处理:
如: url=jdbc:mysql://127.0.0.1:3306/db5?characterEncoding=UTF-8&rewriteBatchedStatements=true
第二步:使用Statement接口或者Preparedstatement接口
保存将执行的SQL语句,执行保存的SQL语句
增加批处理:addBatch(String sql) 将要执行的SQL先保存在当前命令列表中,先不执行
执行批处理:int[] executeBatch() 执行SQL语句,将批处理中所有SQL语句执行,
返回一个数组,这个数组是说明每条命令所影响的行数
清空批处理:clearBatch() : 清空当前批处理的语句
①数据库参数配置优化:
很多配置默认参数(如最大连接数、数据库占用的内存等),不是最佳的配置,例如最大连接数默认为100,当请求超过100时都要等待。
②分库分表:
后面第五题有讲
③优化表结构的设计:
包括字段数据类型,比如人的年龄用无符号的unsigned tinyint即可,没必要用integer。数据类型的长度比如用户的手机号11位长度,没必要用255个长度。有外键约束会影响插入和删除性能,如果程序能够保证数据的完整性,那在设计数据库时就去掉外键。
④SQL语句的优化:
其中最重要的方式就是使用索引,还有#{}防止sql注入
⑤事务优化:
避免大事务,操作的数据比较多的事务会带来风险:锁定太多的数据,造成大量的阻塞和锁超时,回滚时所需时间比较长,执行时间长容易造成主从延迟
服务器层面
①主从复制,读写分离: 一台MySQL服务器同一时间点支持的并发数是有限的,所以增加MySQL服务器的数量也是一种增强数据库性能的方式。使用MySQL主从复制,增删改操作走Master主服务器,查询走Slaver从服务器,这样就减少了只有一台MySQL服务器的压力。
②增加缓存层
增加缓存层,减少数据库连接也是一种优化手段,有些查询可以不用访问数据库,可以通过使用缓存服务器如redis、memcache、elasticsearch等增加缓存,减少数据库的连接
③升级服务器硬件
更快的磁盘IO设备,更强的CPU,更大的内存,更大的网卡流量(带宽)等。
①使用索引, 在SQL语句的WHERE和JOIN部分中用到的所有字段上,都应该加上索引。
②尽量防止索引失效,下面第七题有说索引失效原因
③分页查询优化 ,如果数据量较大,一次性获取所有数据会导致性能问题。应该通过使用LIMIT关键字,进行分页查询。
④where后面条件查询只在自己需要的范围查询
⑤select后面尽量不要使用 * 只选择你需要的字段
⑥OR改写成IN ,OR的效率是n级别,IN的效率是log(n)级别,IN的个数建议控制在200以内;
⑦能使用BETWEEN不用IN ,SELECT id FROM t WHERE num BETWEEN 1 AND 5;
⑧使用INNER JOIN代替子查询、使用EXISTS或NOT EXISTS代替IN和NOT IN等。
使用explain 执行计划进行优化,使用explain关键字可以知道MySQL是如何处理你的SQL语句以及表结构的性能瓶颈。使用方法,在select语句前加上EXPLAIN就可以了。如:
EXPLAIN select * from TableName;
这篇文章写的很好,看这个就行SQL优化之EXPLAIN执行计划
分库:就是一个数据库分成多个数据库,部署到不同机器。
①业务量剧增,MySQL单机磁盘容量会撑爆,在高并发的场景下大量请求访问数据库,MySQL单机是扛不住的!分库可以增加磁盘容量,应对高并发,。
垂直分库:比如原来的数据库有用户表,订单表、积分表、商品表把他们拆分成用户库、订单库、积分库、商品库。
水平分库:将表的数据量切分到不同的数据库服务器上,每个服务器具有相同的库和表,只是表中的数据集合不一样。
分表:就是一个表分成多个表。
①数据量太大的话,一个查询SQL没命中索引,千百万数据量的表可能会拖垮这个数据库。
②高度为3的B+树可以存差不多千万的数据,单表数据量超过千万,就需要分表了否则磁盘IO次数会增多。
垂直分表:用户表包含id、name、age、email、desc,如果email、desc等字段不常用,我们把它拆分到另外一张表,命名为用户详细信息表。
水平分表:一个表的数据量太大,可以按照规则(如hash取模、时间范围等),把数据切分到多张表,将常用数据从表中独立出来。比如 在登录业务中只需要查询用户名和密码。
①索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。
②根据存储方式分为:
聚集索引(InnoDB引擎)
主键索引属于聚簇索引的叶子节点会存储指针的值和数据行,也就是说数据和索引是在一起,这就是聚簇索引,InnoDB中也只有主键索引才能是聚簇索引。
非聚集索引(MyISAM引擎)
二级索引(辅助索引)属于非聚簇索引,叶子节点只会存储数据行的指针,简单来说数据和索引不在一起,就是非聚聚簇索引;
③根据索引特点可以分为:主键索引,唯一索引,普通索引,多列索引,前缀索引,全文索引
④索引就像目录,索引利用特定数据结构存储避免进行全表扫描。mysql默认索引结构B+树,特点是一个节点可以有多个分支节点,同时每个分支节点只存储key而不存储数据,只在叶子节点存储数据和key。这样大大降低了查找深度,减少了磁盘IO次数。
①索引列作为计算的一部分或者使用函数,那么索引会失效。例如,下面这个查询无法使用 actor_ id 列的索引:
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
②不符合最左匹配原则,例如定义了 (a,b,c) 联合索引,相当于构造了 (a)、(a,b)、(a,b,c) 索引。如果要使 c 索引实际工作,那么必须在 WHERE 中同时加入 a、b 字段的条件,顺序无所谓。
③WHERE 子句的查询条件里使用了比较操作符 LIKE和正则,第一个字符不是通配符的情况下才能使用索引。比如查询条件是 LIKE ‘abc%’,MySQL 将使用索引,如果条件是 LIKE ‘%abc’,MYSQL 将不使用索引。
。常见的范围查找有:LIKE ‘%abc’,和通过 <,>,<= ,\ >=,\ between,!=,或者 <> 操作符做比较。它们都是导致索引失效。
注意事项:IN() 和exists不是范围匹配,而是多个等值匹配,因此并不会导致索引失效。
⑤ 如果 WHERE 子句带有 or 且有字段不属于索引,那么即使其中带索引的字段也不会使用。
⑥使用了select*,大概率会查询非索引列的数据就无法使用覆盖索引了
⑦对查询结果排序时使用order by,不符合索引结构的顺序。
⑧索引列值为null,致COUNT(*)不能走索引
查询诸如SELECT COUNT(*) FROM Table 的时候,因为HASHSET中不能存储空值的,所以优化器不会走索引。
⑨not in 和 not exists使索引失效 而IN() 和exists不是范围匹配,而是多个等值匹配,因此并不会导致索引失效。
⑩字段类型不同 ,比如你的索引字段是varchar型,但是你搜索条件却是 userid=333,那这样索引不生效。
聚集索引(InnoDB引擎)
主键索引属于聚簇索引的叶子节点会存储指针的值和数据行,也就是说数据和索引是在一起,这就是聚簇索引,InnoDB中也只有主键索引才能是聚簇索引。
非聚集索引(MyISAM引擎)
二级索引(辅助索引)属于非聚簇索引,叶子节点只会存储数据行的指针(innoDB是主键),简单来说数据和索引不在一起,就是非聚聚簇索引;
应创建索引的字段
应创建索引的场景
1、作为主键必须有索引;(主键)
2、经常用在连接的列上,主要是一些外键,可以加快连接的速度(外键)
3、经常需要搜索的列上,加快搜索的速度; (需搜索)
4、经常需要排序的列上创建索引,利用索引已经排序,加快排序查询时间;(需排序)
5、 经常需要根据范围搜索的列上,因为索引已经排序,其指定的范围是连续的;(范围)
6、经常使用在WHERE子句中的列上,加快条件的判断速度。(where)
7、如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;
不应创建索引的字段
1、查询中很少用到的列,有索引并不能提高查询速度。
2、取值很少的列,比如数据表中的性别列
3、text, image和bit数据类型的列,这些列的数据量要么相当大,要么取值很少。
4、被频繁更新的列,会提高搜索性能,但是会降低修改性能
要提升查询速度的时候,使用索引。索引出现的目的就是为了提升查询速度。使用索引的过程中我们应该避免索引失效。
limit startIndex,length
startIndex是起始下标,length是长度,limit 0,5 表示从0开始,取长度为5的数据。
limit执行顺序在order by后面
JDBC中进行事务处理
Connection提供了事务处理的方法,通过调用setAutoCommit(false)可以设置手动提交事务;当事务完成后用commit()显式提交事务;如果在事务处理过程中发生异常则通过rollback()进行事务回滚。除此之外还引入了Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。
Spring 中进行事务处理
Spring事务包括编程式事务和声明式事务。
编程式事务需要在代码中手动控制事务的开始、提交和回滚等操作
首先通过transactionManager.getTransaction方法获取事务对象status,然后在try块中执行转账操作,最后通过transactionManager.commit(status)提交事务。如果在转账操作中发生了异常,则会通过transactionManager.rollback(status)回滚事务。
声明式事务则是通过在配置文件中声明事务的切入点和通知等信息来自动控制事务的行为。
声明式的事务管理是用Spring的AOP来实现的;在配置文件中声明事务管理器、事务通知等,然后在需要使用事务的方法上添加事务切面的注解即可。可以配置只读事务和回滚事务(传播行为为REQUIRED)当出现错误后进行回滚操作。在项目中通过aop切入事务到serivce层,这样做能使一次业务逻辑操作,包括几个数据库操作都控制在一个事务中。
下面代码中首先声明了事务管理器transactionManager,然后定义了事务通知tx:Advice,该通知会在transferMoney方法执行时进行事务管理。最后通过aop:config和aop:advisor来将txAdvice应用于transferPointcut定义的切入点上。
声明式事务注解方式
在需要使用事务的方法上添加@Transactional注解,并通过该注解的属性来指定事务的传播机制、隔离级别、超时时间等信息。
在下面的代码中,通过@Transactional注解来声明了事务的传播机制为Propagation.REQUIRED,隔离级别为Isolation.DEFAULT,超时时间为3600秒。在方法执行时,Spring会根据注解中的信息自动管理事务的行为。
指定只读事务的办法为:
bean配置文件中,prop属性增加“read-Only”
或者用注解方式@Transactional(readOnly=true)
相当于将数据库设置成只读数据库,此时若要进行写 的操作,会出现错误。
什么是事务的传播行为:事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法时,事务如何传播。
必须具有事务的上下文 | 不需具有事务上下文 |
---|---|
PROPAGATION REQUIRED 有事务在进行,那么被调用端将在该事务中运行否则的话重新开启一个事务 | PROPAGATION SUPPORTS不需具有事务但如果有一个事务的话,它也可以在这个事务中运行 |
PROPAGATION MANDATORY 必须在一个事务中运行,如果没有事务,将抛出异常表示 | PROPAGATION NOT SUPPORTED 总是不需具有事务,并挂起任何存在的事务。 |
PROPAGATION REQUIRES NEW 没有事务就开启一个新的事务。如果事务已经存在,则将这个存在的事务挂起。 | PROPAGATION NEVER 总是不需具有事务地,如果存在事务,抛出异常 |
PROPAGATION NESTED有事务在运行中,则嵌套在事务中运行,外层事务抛出异常回滚,那么内层事务必须回滚,但是内层事务不影响外层事务。如果封装事务不存在,则同propagation.required的一样 |
1、定时同步:应用程序可以定期将MySQL中的数据同步到Redis中。这种方法的优点是实现简单,缺点是可能会导致数据不一致,因为数据在同步的过程中可能会被修改。
2、实时同步:可以使用消息队列实现MySQL和Redis之间的实时同步。当MySQL中的数据发生变化时,将变更的数据以消息的方式发送到消息队列。触发器或者消息队列会立即通知Redis进行更新。执行对应的数据同步操作。这种方法的优点是实时性高,缺点是实现复杂。
3、双写模式:可以将MySQL和Redis同时写入,确保数据的一致性。这种方法的优点是实现简单,缺点是可能会影响性能。
4、读写分离:可以将MySQL用于写操作,而将读操作从MySQL转移到Redis中。从缓存读取数据,如果读不到从数据库加载然后读入缓存。这种方法的优点是实现简单,提高系统的性能,缺点是可能会导致数据不一致,因为MySQL写操作的数据和Redis读操作的数据之间的同步延迟,与存在一定的延迟。。读写分离是指,只在MySQL中执行写操作。
5.异步写入:异步写入是指先将数据写入MySQL,然后异步将数据写入Redis。这种方法可以提高系统的性能,但是不能保证数据的实时性。
6.利用MySQL的binlog实时同步:通过解析MySQL的binlog,可以获取到数据库的更新操作日志,然后将变更的数据实时同步到Redis中。可以使用Canal等开源工具来实现binlog的解析。
7.利用MySQL的触发器实时同步:在MySQL中创建触发器,当数据发生变更时,触发器会将变更的数据发送到Redis中。这种方式需要在MySQL中编写触发器逻辑。
线程的生命周期是指一个线程从创建到终止所经历的各个阶段。在Java中,线程的生命周期通常包括以下五种状态:
①新建(New):线程被创建时处于新建状态。它还没有开始运行,没有执行任何代码。
②就绪(Runnable):当线程被启动后,它进入就绪状态。此时,线程已经准备好运行,但是操作系统可能还没有分配到处理器资源。
③运行(Running):当操作系统将处理器资源分配给线程时,它进入运行状态。此时,线程正在执行其代码。
④阻塞(Blocked):当线程需要等待某个条件满足(例如等待I/O操作完成)时,它进入阻塞状态。在阻塞状态下,线程会暂时放弃处理器资源,直到等待的条件满足。
⑤终止(Terminated):当线程执行完毕或因异常而终止时,它进入终止状态。此时,线程不再占用任何资源。
synchronized 关键字,代表加锁,保证被它修饰的方法或者代码块只能有一个线程执行。 synchronized是一个对象锁,也就是它锁的是一个对象。我们无论使用哪一种方法,synchronized都需要有一个锁对象,它可以修饰实例方法,修饰静态方法,修饰代码块
①synchronized修饰实例方法, 在方法上加上synchronized关键字即可。进入同步代码前要获得当前实例(调用这个方法的实例)的锁 。
public class SynchronizedTest1 {
public synchronized void test() {
System.out.println("synchronized 修饰 方法");
}
}
②synchronized修饰静态方法,加上synchronized关键字即可, 由于静态方法不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。给静态方法加synchronized锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前静态方法所在类的Class对象的锁。
public static synchronized void test(){
i++;
}
补充:线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
③修饰代码块: synchronized修饰代码块需要传入一个对象。进入同步代码库前要获得给定对象的锁,这里的this 。
public class SynchronizedTest2 {
public void test() {
synchronized (this) {
System.out.println("synchronized 修饰 代码块");
}
}
}
补充;synchronized(object) ,表示进入同步代码库前要获得 给定对象的锁
synchronized(类.class) ,表示进入同步代码前要获得 给定 Class 的锁
注意:
①原子性:确保线程互斥的访问同步代码。synchronized保证只有一个线程拿到锁,进入同步代码块操作共享资源,因此具有原子性。
②可见性:保证共享变量的修改能够及时可见。当某线程进入synchronized代码块前后,线程会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存成为副本,执行代码,将修改后的副本的值刷新回主内存中,线程释放锁。其他获取不到锁的线程会阻塞等待,所以变量的值一直都是最新的。
③有序性:synchronized内的代码和外部的代码禁止排序,至于内部的代码,则不会禁止排序,但是由于只有一个线程进入同步代码块,因此在同步代码块中相当于是单线程的,根据 as-if-serial 语义,即使代码块内发生了重排序,也不会影响程序执行的结果。
④悲观锁:synchronized是悲观锁。每次使用共享资源时都认为会和其他线程产生竞争,所以每次使用共享资源都会上锁。
⑤独占锁(排他锁):synchronized是独占锁(排他锁)。该锁一次只能被一个线程所持有,其他线程被阻塞。
⑥非公平锁:synchronized是非公平锁。线程获取锁的顺序可以不按照线程的阻塞顺序。允许新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待。这样有利于提高性能,但是也可能会导致饥饿现象
⑦可重入锁:synchronized是可重入锁。持锁线程可以再次获取自己的内部的锁,可一定程度避免死锁。
①原子性指的是一个不可以被分割的操作,即这个操作在执行过程中不能被中断,要么全部不执行,要么全部执行。且一旦开始执行,不会被其他线程打断。
②可见性 指的是一个线程修改了共享变量后,其他线程能立即感知这个变量被修改。
③有序性 指程序按照代码的先后顺序执行。在Java内存模型中,为了提升效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。
volatile 是最轻量的同步机制。只能用于变量(类的成员变量、类的静态成员变量),volatile是为了保证多个线程间拿到的变量都是最新的同一个内容,用volatile修饰后使的能够实时同步;
被volatile修饰之后就保证了:
①可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。除了volatile,Java中的synchronized和final两个关键字 以及各种 Lock也可以实现可见性。
②顺序性:禁止进行指令重排序。
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量。
③volatile关键字无法保证原子性,更准确地说是volatile关键字只能保证单操作的原子性,比如 x=1,但是无法保证复合操作的原子性,比如x++
1、volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
2、volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
3、volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
4、volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
5、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
ThreadLocal叫做线程本地变量,ThreadLocal想让线程间对某个变量不相互干扰,隔离开来;ThreadLocal中填充的变量属于当前线程独有的变量,对其他线程而言是隔离的。
①ThreadLocal是JDK包提供的,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题
②ThreadLocal相当于线程间的数据隔离。Synchronized则是为用于线程间的共享数据加锁。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
③ThreadLocal 适用于变量在线程间隔离而在方法或类间共享的场景。最常见的ThreadLocal使用场景为用来解决数据库连接、Session 管理等
threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值,不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的。
①线程池介绍
线程池是一种多线程处理形式,可以理解为多个线程的集合。线程是需要时就创建,执行完任务就销毁,而线程池是取用一个创建好的线程,用完就放回去。
//线程池(JDK)
ExecutorService executorService = Executors.newFixedThreadPool(16);
//千人并发访问
for(int i=0;i<1000;i++){
executorService.submit(new Runnable() {
@Override
public void run() {
studentService.queryAllStudentCount();
}
});
}
②线程池优势
1、降低系统资源消耗,提高系统响应速度:通过重用已存在的线程,降低线程创建和销毁造成的消耗;无需等待新线程的创建便能立即执行;
2、线程并发数的管控,统一的分配,调优和监控。因为线程若是无限制的创建,可能会产生OOM(内存溢出),并且会造成cpu过度切换。
3、提供更强大的功能,延时定时线程池。
核心线程数量、最大线程数、等待队列数、每一个线程的执行时间、线程的名称等参数的线程。
参数 | 释义 |
---|---|
corePoolSize | 线程池中的常驻核心线程数。当有请求任务来之后,若线程池已创建的线程数小于corePoolSize,会通过创建一个新线程来执行该任务。当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。 |
maximumPoolSize | 线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。 |
keepAliveTime | 空闲线程的存活时间。当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。 |
unit | keepAIiveTime的单位 |
workQueue | 任务队列。用于传输和保存等待执行任务的阻塞队列 |
threadFactory | 线程工厂。用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号) |
handler | 线程饱和策略。当线程池和队列都满了,再加入线程会执行此策略。 |
策略 | 释义 |
---|---|
CallerRunsPolicy | 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。 |
AbortPolicy | 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。 |
DiscardPolicy | 直接丢弃,其他啥都没有。 |
DiscardOldestPolicy | 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入。 |
创建线程池的方式 (使用Executors顶层容器静态类实现创建)
Executory.newFixedThreadPool(int);//创建固定容量大小的线程池
线程池有几种
种类 | 释义 |
---|---|
newFixedThreadPool | 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。(优点是适合长期任务,性能好。缺点是不会复用线程,每运行一个Runnable都会通过ThreadFactory创建一个线程) |
newScheduledThreadPool | 创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。 |
newCachedThreadPoo | 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。(优点是适合短期异步任务或者负载很轻的服务。缺点是可以无限的新建线程,容易造成堆外内存溢出。) |
newSingleThreadExecutor | 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。(优点是一个任务一个任务执行,保证顺序性。缺点是前一个任务的延迟或异常都将会影响到之后的任务) |
①线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。
②当调用 execute() 方法添加一个任务时,线程池会做如下判断:
1、如果正在运行的线程数量小于核心线程数,那么马上创建线程运行这个任务;
2、如何大于或等于核心线程数,那么将这个任务放入队列。
3、如果队列满了,而且正在运行的线程数量小于最大线程数,那么还是要创建线程运行这个任务;
4、如果队列满了,而且正在运行的线程数量大于或等于 最大线程数,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
③当一个线程完成任务时,它会从队列中取下一个任务来执行。
④当一个线程超过一定的时间(keepAliveTime)没事做,并且当前运行的线程数大于核心线程数,那么这个线程就被停掉。
①AQS是什么?
AQS定义了一套多线程访问共享资源的同步器框架。 来实现同步类(Lock、Semaphore、ReentrantLock等)。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。
Spring的异步处理。如果是使用Spring Boot项目那只需要2个注解就能搞定了。如下:
第一步:基于Springboot启动类启用加@EnableAsync注解
@EnableAsync
@SpringBootApplication
public class AsyncApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncApplication.class, args);
}
}
第二步在要使用的方法上加@Async注解, Spring应用默认的线程池为SimpleAsyncTaskExecutor,指在@Async注解在使用时,不指定线程池的名称。
@Slf4j
@Service
public class BusinessServiceImpl implements BusinessService {
/**
* 方法4:没有指定线程池,验证默认线程池也ok(不推荐:规避资源耗尽的风险)
*/
@Async
public void asyncDemo4() {
log.info("asyncDemo4:" + Thread.currentThread().getName() + " 正在执行 ----------");
try {
Thread.sleep(2*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("asyncDemo4:" + Thread.currentThread().getName() + " 执行结束!!");
}
}
也可以自己配置一个线程池 ThreadPoolTaskExecutor详细内容看这篇文章
意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。
// **************************Synchronized的使用方式**************************
// 1.用于代码块
synchronized (this) {}
// 2.用于对象
synchronized (object) {}
// 3.用于方法
public synchronized void test () {}
// 4.可重入
for (int i = 0; i < 100; i++) {
synchronized (this) {}
}
// **************************ReentrantLock的使用方式**************************
public void test () throw Exception {
// 1.初始化选择公平锁、非公平锁
ReentrantLock lock = new ReentrantLock(true);
// 2.可用于代码块
lock.lock();
try {
try {
// 3.支持多种加锁方式,比较灵活; 具有可重入特性
if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
} finally {
// 4.手动释放锁
lock.unlock()
}
} finally {
lock.unlock();
}
}
实现多线程可以通过继承Thread类和实现Runnable接口。
(1)继承Thread
定义一个类继承Thread类,复写Thread类中的public void run()方法,将线程的任务代码封装到run方法中,直接创建Thread的子类对象,创建线程。调用start()方法,开启线程(调用线程的任务run方法)
(2)实现Runnable接口;
定义一个类,实现Runnable接口;覆盖接口的public void run()的方法,将线程的任务代码封装到run方法中;创建Runnable接口的子类对象,将Runnabl接口的子类对象作为参数传递给Thread类的构造函数,创建Thread类对象调用start()方法,启动线程。
区别:
(1)实现Runnable接口避免了单继承的局限性
(2)继承Thread类线程代码存放在Thread子类的run方法中,实现Runnable接口线程代码存放在接口的子类的run方法中;
那使用到多线程的时候就必须要先对线程场景进行一个解构,也就是对单体请求如何合理的拆分成多线程请求
多人订阅服务,防止超出数量限制
使用数据库锁或分布式锁控制并发问题,从而控制减库存的冲突,避免超卖问题
①数据库分布式锁可以采用乐观锁,但由于数据库本身的性能和并发处理能力并不理想,在高并发项目中,使用数据库锁也是不合适的;
②基于Redis实现的分布式锁,由于Redis是单线程的,不管有多少个并发请求,Redis会将请求排队进行处理,即一个一个地有先后顺序地处理,这样即不会有并发问题,即不会产生减库存的冲突,从而解决减库存的超卖问题;
页面上需要导出excel文件
异步。比如页面上需要导出excel文件,由于业务要求,需要导出全部数据需要10分钟左右。如果使用同步的方式,用户需要在这个导出页面等待10分钟,不能做其它操作,这样肯定是不行的。那么可以采用异步,用户点击导出,导出接口主线程往数据库当中插入一条导出记录,开个子线程获取数据,写入Excel文件,完成之后更新导出记录,当中,然后主线程直接给用户返回。这样用户点击导出会生成一个导出记录信息,不用在这里等待,等导出完成,之后可以在导出记录当中进行下载。
异步导出实现
ApacheBench 是 Apache 服务器自带的一个web压力测试工具,简称ab。
ab又是一个命令行工具,对发起负载的本机要求很低,根据ab命令可以创建很多的并发访问线程,模拟多个访问者同时对某一URL地址进行访问,执行命令 ab -n -c url
包含锁与同步,等待通知机制,信号量,管道
第五章线程间通讯
redis默认16个库,redus默认端口号6379
数据类型以及应用场景:
String:简单的key-value: 缓存、计数器、分布式锁等。
List列表: 链表、队列、微博关注人时间轴列表等。
Hash字典: 用户信息、Hash 表等。
Set集合: 去重、赞、踩、共同好友等。
Zset(sorted set)有序集合: 访问量排行榜、点击量排行榜等。
①我们项目是一直迭代升级,一开始没有使用Redis的时候,我们所有数据都是直接到达数据库获取,导致我们后端的数据库经常出现cpu及io压力很大,后续我们将前端业务系统上一些不需要实时更新的数据,一些频繁查询的热点数据,进行了Redis缓存存储,来提升系统的能力。
②会话缓存(Session Cache)
用户登录Web应用时候,将会话数据存储于Redis,并将唯一的会话ID(Session ID)返回到客户端的Cookie中。当用户再向应用发送请求时,会将此会话ID包含在请求中。无状态的Web服务器,根据这个会话ID从Redis中搜索相关的会话数据来进一步请求处理。
③排行榜/计数器
Redis提供了排序集合(Sorted Sets)的功能,排序集合是唯一元素(比如:用户id)的集合,每个元素按分数排序,这样可以快速的按分数来检索元素
优点:
快速:Redis使用内存存储数据,读写速度非常快。
多种数据类型:Redis支持多种数据结构,可以适应不同的应用场景。
丰富的特性:Redis支持事务、Lua脚本、发布订阅模式等高级特性。
高可用高扩展性:Redis可以通过主从复制、哨兵模式和集群模式等方式实现高可用和横向扩展。
缺点
内存限制:由于Redis使用内存存储数据,因此受到内存容量的限制。
持久化问题:Redis默认不会将数据持久化到硬盘,需要使用持久化机制来解决数据丢失问题。
单线程模型:Redis采用单线程模型,虽然可以通过多实例和多线程方式解决并发问题,但是并发能力相对较弱。
缓存穿透是什么
缓存穿透是指查询一个不存在的数据,由于缓存中没有数据,请求会直接穿透到数据库中,从而引起数据库的压力过大,严重影响系统的性能。
解决缓存穿透的常用方法有两种:①布隆过滤器
是一种高效的数据结构,可以判断一个元素是否存在于一个集合中,同时也可以减轻数据库的压力。在使用布隆过滤器的时候,首先将所有的数据hash到一个位图中,如果查询的数据在位图中不存在,那么直接返回不存在,从而避免了对数据库的查询操作。
在SpringBoot中,我们可以使用Guava提供的布隆过滤器实现缓存穿透的解决方案。例如:
@Bean
public BloomFilter bloomFilter() {
return BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 100000, 0.001);
}
@Override
public User getUserById(String id) {
// 先从布隆过滤器中查询是否存在。布隆过滤器中没有直接返回null
if (!bloomFilter.mightContain(id)) {
return null;
}
// 如果存在,则查询Redis中的缓存数据
User user = redisTemplate.opsForValue().get(id);
if (user == null) {
// 如果Redis中不存在,则查询数据库
user = userDao.getUserById(id);
if (user != null) {
// 将数据缓存到Redis中
redisTemplate.opsForValue().set(id, user);
} else {
// 如果数据库中也不存在,则将该id加入到布隆过滤器中
bloomFilter.put(id);
}
}
return user;
}
②空对象缓存并设置过期时间
当查询的数据不存在时,将一个空对象缓存到Redis中。这样下次查询同样不存在的数据时,就可以直接从Redis中获取到一个空对象,从而避免了对数据库的查询操作。
在SpringBoot中,我们可以通过设置Redis缓存的过期时间来实现空对象缓存的解决方案。例如:
@Override
public User getUserById(String id) {
User user = redisTemplate.opsForValue().get(id);
if (user == null) {
// 如果Redis中不存在,则查询数据库
user = userDao.getUserById(id);
if (user != null) {
// 将数据缓存到Redis中
redisTemplate.opsForValue().set(id, user);
} else {
// 如果数据库中也不存在,则将一个空对象缓存到Redis中,设置五分钟过期时间防止缓存雪崩
redisTemplate.opsForValue().set(id, new User(), 5, TimeUnit.MINUTES);
}
}
return user;
}
缓存击穿是什么
缓存击穿是指一个非常热点的数据在缓存中过期之后,正好在这个时间段内有大量的请求访问该数据,这些请求会直接穿透到数据库中,从而引起数据库的压力过大
两种解决方式①设置热点数据永不过期
缺点:热点数据可能会被修改,如果不及时更新缓存,可能会导致缓存中的数据与实际数据不一致
@Override
public User getHotUserById(String id) {
User user = redisTemplate.opsForValue().get(id);
if (user == null) {
// 如果Redis中不存在,则查询数据库
user = userDao.getHotUserById(id);
if (user != null) {
// 将数据缓存到Redis中,设置过期时间为1小时
redisTemplate.opsForValue().set(id, user, 1, TimeUnit.HOURS);
}
}
return user;
}
②双写策略
有大量请求在缓存中查询数据时,1、先在缓存中写入一个空对象,2、然后让一个请求去查询数据库,然后写入缓存,其余请求都去查询缓存。
在SpringBoot中,我们可以通过设置Redis缓存的过期时间来实现延迟缓存双写策略的解决方案。例如:
@Override
public User getHotUserById(String id) {
User user = redisTemplate.opsForValue().get(id);
if (user == null) {
// 如果Redis中不存在,则写入一个空对象
redisTemplate.opsForValue().set(id, new User(), 5, TimeUnit.MINUTES);
// 去数据库中查询数据并更新缓存
user = userDao.getHotUserById(id);
if (user != null) {
redisTemplate.opsForValue().set(id, user, 1, TimeUnit.HOURS);
}
}
return user;
}
缓存雪崩是什么
缓存雪崩是指当缓存中的大量数据在同一时间失效,导致大量请求直接访问数据库,从而引起数据库的压力过大,严重影响系统的性能。
两种解决方式①缓存数据的随机过期时间
增加随机因素,从而避免大量数据在同一时间失效的情况。在SpringBoot中,我们可以通过设置Redis缓存的过期时间和一个随机值来实现这个解决方案。例如:
@Override
public List<User> getUserList() {
List<User> userList = redisTemplate.opsForValue().get("userList");
if (userList == null) {
// 如果Redis中不存在,则查询数据库
userList = userDao.getUserList();
if (userList != null && userList.size() > 0) {
// 将数据缓存到Redis中,并增加随机的过期时间
int random = new Random().nextInt(600) + 600;
redisTemplate.opsForValue().set("userList", userList, random, TimeUnit.SECONDS);
}
}
return userList;
}
②预热缓存
将系统中的热点数据提前加载到缓存中,从而避免了大量请求同时访问数据库的情况。在SpringBoot中,我们可以通过编写一个启动时执行的方法,来实现预热缓存的解决方案。例如:
@Component
public class CacheInit implements CommandLineRunner {
@Autowired
private UserDao userDao;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void run(String... args) throws Exception {
List<User> userList = userDao.getUserList();
if (userList != null && userList.size() > 0) {
// 将数据缓存到Redis中,并设置过期时间为1小时
for (User user : userList) {
redisTemplate.opsForValue().set(user.getId(), user, 1, TimeUnit.HOURS);
}
}
}
}
③分布式锁
使用分布式锁,从而避免大量请求同时访问数据库的情况。在SpringBoot中,我们可以通过Redisson来实现分布式锁的解决方案。例如:
@Override
public List<User> getUserList() {
List<User> userList = redisTemplate.opsForValue().get("userList");
if (userList == null) {
// 如果Redis中不存在,则尝试获取分布式锁
RLock lock = redissonClient.getLock("userListLock");
try {
// 尝试加锁,并设置锁的过期时间为5秒
boolean success = lock.tryLock(5, TimeUnit.SECONDS);
if (success) {
// 如果获取到了锁,则查询数据库并将数据缓存到Redis中
userList = userDao.getUserList();
if (userList != null && userList.size() > 0) {
redisTemplate.opsForValue().set("userList", userList, 1, TimeUnit.HOURS);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock
}
} return userList;
}
持久化方式
RDB 持久化:该机制可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。
AOF 持久化:记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。
同时应用 AOF 和 RDB:当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。
RDB优缺点
RDB 优点:RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份,RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
RDB 缺点:你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。
AOF优缺点
优点:
1、AOF 文件是一个只进行追加操作的日志文件。AOF 的默认策略为每秒钟 fsync(文件同步) 一次,并且就算发生故障停机,也最多只会丢失一秒钟的数据。Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。
缺点:
对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。 AOF在恢复大数据集时的速度比 RDB 的恢复速度要慢。
①对于设置了过期时间的
volatile-ttl 越早过期的越先被删除。
volatile-random 设置了过期时间的键值对中,进行随机删除。
volatile-lru(Least Recently Used) 设置了过期时间的键值对中,选择最长时间没有使用的键删除。
volatile-lfu((Least Frequently Used) ) 设置了过期时间的键值对中,选择最少使用次数的键进行删除。
②对于所有的
allkeys-lru:从所有的键中,选择最长时间没有使用的键进行删除。
allkeys-lfu,从所有的键中,选择最少使用次数的键进行删除。
allkeys-random:从所有的键中,随机选择一个进行删除。
noeviction:默认是禁止淘汰,如果数据达到了最大内存限制,写入数据时会报错。
用户点击后我们会将用户的提交在redis中存放一个标志,如果用户重复提交,我们会检查redis的标志,来拒绝用户的第二次提交,只处理第一次提交请求;
①先需要在Linux服务器上部署环境,安装jdk,安装MySQL,redis服务器和tomact服务器,nginx服务器。
②用maven打包项目,将项目打成war包。
③使用xshell工具,把war包上传到linux服务器,将项目传到tomcat的webapp文件夹里面。
④启动tomcat,启动MySQL服务,就可以访问系统了
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
StudentMapper studentMapper;
@Autowired(required = false)
RedisTemplate redisTemplate;
@Override
//public synchronized Long queryAllStudentCount() {
public Long queryAllStudentCount() {
//key序列化,对数据没有任何影响
redisTemplate.setKeySerializer(new StringRedisSerializer());
//查询缓存
Long allStudentCount = (Long)redisTemplate.opsForValue().get("allStudentCount");
if(allStudentCount==null){
synchronized (this){
//查询缓存
allStudentCount = (Long)redisTemplate.opsForValue().get("allStudentCount");
if(allStudentCount==null){
System.out.println("---查询数据库---");
//查询数据库
allStudentCount=studentMapper.selectAllStudentCount();
//放入缓存
redisTemplate.opsForValue().set("allStudentCount", allStudentCount, 20, TimeUnit.SECONDS);
}else{
System.out.println("---缓存命中---");
}
}
}else{
System.out.println("---缓存命中---");
}
return allStudentCount;
}
}
Springboot 整合 SpringCache使用redis缓存
linux常用命令
主从复制: Redis 支持主从复制机制,其中一个 Redis 实例作为主节点,负责写操作,而其他实例作为从节点,负责复制主节点的数据。这种方式可以实现数据的备份和负载均衡,从而提高可靠性和性能
1.数据复制:主节点将写操作记录在本地的操作日志(AOF文件或RDB文件)中,并定期将这些操作发送给从节点。从节点接收到操作后,对其进行重放,从而保持与主节点的数据一致性。
2.异步复制:主节点将写操作发送给从节点时,是异步的,即主节点不会等待从节点的确认。这样可以提高性能,但在故障发生时可能导致部分数据丢失。
3.读写分离:主节点负责写操作,而从节点负责读操作。这样可以分担主节点的负载,并提供更好的读性能。
Sentinel 哨兵: Redis Sentinel 是一个监控和自动故障恢复系统
Sentinel 系统有三个主要任务:
①监控:Sentinel 不断的检查主服务和从服务器是否按照预期正常工作。
②提醒:被监控的 Redis 出现问题时,Sentinel 会通知管理员或其他应用程序。
③自动故障转移:监控的主 Redis 不能正常工作,Sentinel 会开始进行故障迁移操作。将一个从服务器升级新的主服务器。 让其他从服务器挂到新的主服务器。同时向客户端提供新的主服务器地址。
①redis是内存数据库,纯内存访问避免了磁盘的 I/O 等耗时操作
②redis 为单线程不会因为线程创建导致的性能消耗,没有多线程切换的开销,减少了锁竞争,以及频繁创建线程和销毁线程的代价。Redis 的单线程指的是 Redis 的网络 IO 以及键值对指令读写是由一个线程来执行的。 对于 Redis 的持久化、集群数据同步、异步删除等都是其他线程执行。
③采用了 I/O 多路复用机制,非阻塞I/O大大提升了并发效率
④高效的数据结构:这里所说的数据结构并不是 Redis 提供给我们使用的 5 种数据类型:String、List、Hash、Set、SortedSet。而是这些数据类型底层的数据结构
@Configuration 注解为例,它用来标记类可以当做一个 bean 的定义,被 Spring IOC 容器使用。另一个例子是@Bean 注解,它表示此方法将要返回一个对象,作为一个 bean 注册进 Spring 应用上下文。
@Autowired:该注解应用于依赖注入
@SpringBootApplication
这个注解是springboot启动类上的一个注解,是一个组合注解,它的主要作用就是标记说明这个类是springboot的主配置类,springboot可以运行这个类里面的main()方法来启动程序此注解相当于@Configuration、@EnableAutoConfiguration和@ComponentScan的组合。
@RestController
相当于是@Controller和@ResponseBody的组合注解。返回json数据不需要在方法前面加@ResponseBody注解了,但使用@RestController这个注解,就不能返回jsp,html页面,视图解析器无法解析jsp,html页面
@Field
使用 @Field 注解时,Spring Boot 会根据注解中指定的属性名,在请求参数中查找同名的参数值,并将其自动转换为该属性的类型,然后赋值给该属性。需要注意的是,@Field 注解只能用于处理表单数据和 URL 查询参数,对于 JSON 数据,需要使用 @RequestBody 注解或其他相关注解进行处理。
public class User {
private String name;
private int age;
// getter 和 setter 方法省略
}
@RestController
public class UserController {
@PostMapping("/user")
public void createUser(@Field("name") String name, @Field("age") int age) {
User user = new User();
user.setName(name);
user.setAge(age);
// 保存用户信息到数据库
}
}
我们使用定时任务主要是处理一些定时或延迟的工作,配置好时间,让程序在指定的时间或者指定的频率去执行,使用Spring Task,Spring 3.0后提供Spring Task实现任务调度,支持按日历调度,相比Quartz功能稍简单,但是在开发基本够用,支持注解编程方式。
SpringBoot中的Schedule : 通过@EnableScheduling+@Scheduled最实现定时任务,底层使用的是Spring Task
F:\2023动力节点三四阶段\03-javaweb\007-project\05-视频\day05
第一阶段有bean定义,先在配置文件里面用
第二阶段bean的初始化,通过构造器或工厂方法创建 Bean 实例再通过反射机制为 Bean 的属性设置值,创建后使用初始化方法(init-method)进行初始化。再将 Bean 实 例 传 递 给 Bean 后 置 处 理 器
第三阶段bean的运行期,此时Bean已经被创建准备好可以使用了。
第四阶段bean的销毁,当容器关闭时, 调用 Bean 的销毁方法(destroy-method)进行销毁
mybatis里xml文件想返回list〈map〉数据类型/或者list〈bean〉,select标签里需要怎么写
我对SpringCloud了解的不是很深,他里面有很多组件,用来解决不同的问题,Eureka组件用来注册服务管理服务,还有Feign用来做服务调用,还有现在比较新的nacos.我自己就用过eureka和nacos。
其主要涉及的组件包括:
Hystix:熔断器
Eureka:注册中心
Zuul:服务网关
Ribbon:负载均衡
Feign:服务调用
sentinel
服务降级: 微服务架构中在服务器压力大的时候,对一些边缘服务进行降级(例如返回一个提前准备好的错误处理信息),让核心业务享受更多资源,能够正常运行。
雪崩效应: 微服务之间的数据交互是通过远程调用来完成的。服务A调用B,B调用服务C,当服务C的调用响应时间过长或者服务C不可用,对C的调用越来越多,然后服务C崩溃了,但是链路调用还在,对服务B的调用也在持续增多,然后服务B崩溃,随之A也崩溃,就是雪崩效应。
服务熔断: 应对微服务雪崩效应的保护机制,类似保险丝。当调用链路的某个微服务不可用或者响应时间太长时,会进行服务熔断,不再有该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
服务熔断的实现
在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。
服务熔断和服务降级的区别
①服务熔断由链路上某个服务引起的,服务降级是从整体的负载考虑
②服务熔断一般是自我熔断恢复,服务降级相当于人工控制
③服务熔断是一个框架层次的处理,服务降级是业务层次的处理
Eureka是spring cloud的服务注册和发现的组件
首先导入eureka的包依赖,然后在springboot的启动类加上Eureka的启动注解@EnableDiscoveryClient让这个服务能被发现 ,最后在配置文件properties里加上eureka相关的配置,比如端口配置。这样服务就可以被注册到Eureka的服务中心。我们可以通过eureka的ui去看服务的启动情况。
两个用法一样,但是Eureka已经停止维护了,Nacos是更新版本的用来做服务注册和发现的组件。它的功能更完善,发送心跳次数更频繁,监测服务心跳更准确。
Gateway是一个网关技术,他的作用是用来过滤一些不满足条件的请求,客户端发送到 Spring Cloud Gateway 的请求需要通过一定的匹配条件,才会发送到真正的服务节点。
具体使用是:引入Gateway相关依赖包以后,通过在yml配置文件里定义filters(过滤器)来设置过滤条件对请求进行拦截和修改,通过 Predicate (断言)来实现 Route 路由的匹配。简单点说,Predicate 是路由转发的判断条件,请求只有满足了 Predicate 的条件,才会被转发到指定的服务上进行处理。
OpenFegin 是用来进行远程服务调用的组件,服务被注册到服务中心后,通过使用OpenFeign,我们可以像调用本地方法一样来调用远程服务。
先导入OpenFeign的依赖,在启动类上使用@EnableFeignClients这样就会对有@FeignClient注解的包进行扫描,接下来在调用端新建被调用端的接口映射,这个映射上要加@FeignClient注解。
我理解的nginx是用来做反向代理的:本来客户端可以直接通过HTTP协议访问应用服务器,用了nginx后客户端所有请求都到了nginx,然后nginx发送请求到服务器,并将从服务器上得到的结果返回给客户端。我们通过配置来管理这些请求应该具体发送到哪些地址。
先找redis中安装nginx服务器然后启动,在conf文件里配置监听的服务端口,地址。配置转发的地址。
为了解决负载均衡服务器的宕机,需要建立一个备份机。主服务器和备份机上都运行高可用(High Availability)监控程序,通过传送诸如“I am alive”这样的信息来监控对方的运行状况。当备份机不能在一定的时间内收到这样的信息时,它就接管主服务器的服务IP并继续提供负载均衡服务;当备份管理器又从主管理器收到“I am alive”这样的信息时,它就释放服务IP地址,这样的主服务器就开始再次提供负载均衡服务。
如果是传统的集中式架构在注册时,我们会做出如下操作:
1.收集用户录入信息,保存到数据库
2.向用户的手机或邮箱发送验证码等等…
实现这个功能非常简单:开启一个本地事务,往本地数据库中插入一条用户数据,发送验证码,提交事物。但是在分布式架构中,用户和发送验证码是两个独立的服务,它们都有各自的数据库,那么就不能通过本地事务保证操作的原子性。这时我们就需要用到 MQ(消息队列)来为我们实现这个需求。在用户进行注册操作的时候,我们为该操作创建一条消息,当用户信息保存成功时,把这条消息发送到消息队列。验证码系统会监听消息,一旦接受到消息,就会给该用户发送验证码。
RabbitMQ是什么: 是消息队列,主要目的是管理消息的传输和提升效率,整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。mq有三大优点:解耦,异步,削峰.
生产者和消费者 : 生产消息的一方(邮件投递者),消费消息的一方(邮件收件人)
消息:
由消息头和 消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routingkey(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。生产者把消息交由 RabbitMQ 后,RabbitMQ 会根据消息头把消息发送给感兴趣的 Consumer(消费者)
Exchange(交换器):
在 RabbitMQ 中,消息并不是直接被投递到消息队列中的,中间还必须经过 Exchange(交换器) 这一层,Exchange(交换器) 会把我们的消息分配到对应的消息队列中。
Exchange(交换器) 有 4 种类型,不同的类型对应着不同的路由策略:direct(默认),fanout, topic, 和 headers。
BindingKey和RoutingKey
BindingKey (绑定)就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。
生产者将消息发给交换器的时候,一般会指定一个 RoutingKey(路由键),用来指定这个消息的路由规则,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候。
Queue(消息队列)
Queue(消息队列) 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
Broker(消息中间件的服务节点)
一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。
例如:前端重复提交
用户注册,用户创建商品等操作,前端都会提交一些数据给后台服务,后台需要根据用户提交的数据在数据库中创建记录。如果用户不小心多点了几次,后端收到了好几次提交,这时就会在数据库中重复创建了多条记录。这就是接口没有幂等性带来的 bug。
例如:消息重复消费
在使用消息中间件来处理消息队列,且手动 ack 确认消息被正常消费时。如果消费者突然断开连接,那么已经执行了一半的消息会重新放回队列。
当消息被其他消费者重新消费时,如果没有幂等性,就会导致消息重复消费时结果异常,如数据库重复数据,数据库数据冲突,资源重复等。
1、消息去重
消息去重是一种常用的解决重复消费问题的方法。可以通过给每条消息设置唯一的消息 ID,并将已消费的消息 ID 进行保存,当接收到重复的消息时,先判断该消息 ID 是否已经消费过,若已消费过则直接丢弃该消息。为了保证消息 ID 的唯一性,可以使用 UUID 等方式生成
2、幂等性处理
在消息消费者端,可以通过实现幂等性处理来解决重复消费问题当接收到重复的消息时,先判断该消息是否已经被处理过,若已经处理过则直接跳过该消息。幂等性处理可以通过在数据库中添加唯一索引、使用乐观锁等方式来实现。
3、消费者限流
消费者限流是一种控制消费速度的方法,可以有效地减少重复消费的可能性。在 RabbitMQ 中,可以通过设置消费者的 prefetchcount 消费速度。
幂等性问题的思考和总结,防重、幂等
幂等性和幂等性接口:
幂等性:多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。幂等性接口是指可以使用相同参数重复执行,并能获得相同结果的接口。
防重和幂等的区别:
防重设计主要为了避免产生重复数据,对接口返回没有太多要求。
而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。
可以使用 写入 Redis 来保证,因为 Redis 的 key 和 value 就是天然支持幂等的。当然还有使用 数据库插入法 ,基于数据库的唯一键来保证重复数据不会被插入多条。
①生产者到rabbitMQ:保障投递者投递消息可靠
1、Confirm模式来实现异步发布确认,即确认消息是否成功,如果发送确认失败,则重新发送消息,如果补偿也不成功,则需要把丢失的消息存储到数据库(缓存中),推送人工处理。
2、事务机制和 Confirm 机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。
②rabbit本身可靠性
1、把消息持久化下来
2、服务集群搭建
③可能消费真正消费时消息丢失
1、把自动确认机制改为手动确认,保障消息真正消费完再进行消息的确认删除
2、basicAck 机制、死信队列、消息补偿机制
RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。
单机模式
Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式。
普通集群模式
意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。
镜像集群模式
这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了
这样的好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据
当消息在一个队列中变成死信之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX(Dead-Letter-Exchange)死信交换器,绑定 DLX 的队列就称之为死信队列。
死信的几种原因
①消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。
②消息 TTL 过期。
③队列满了,无法再添加
延迟队列指的是存储对应的延迟消息,消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
实现延迟消息,一般有两种方式:
通过 RabbitMQ 本身队列的特性来实现,需要使用 RabbitMQ 的死信交换机(Exchange)和消息的存活时间 TTL(Time To Live)。在 RabbitMQ 3.5.7 及以上的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列功能。同时,插件依赖 Erlang/OPT 18.0 及以上。
RabbitMQ 自 V3.5.0 有优先级队列实现,优先级高的队列会先被消费。可以通过x-max-priority参数来实现优先级队列。不过,当消费速度大于生产速度且 Broker 没有堆积的情况下,优先级显得没有意义。
①简单模式:一个生产者一个消费者绑定一个队列
②work 工作模式:一个生产者多个消费者消费同一个队列
③pub/sub 发布订阅模式:一个生产者,一个交换机,多个队列,多个消息消费者。
④Routing 路由模式:一个生产者,一个交换机,多个队列,多个消费者。
⑤Topic 主题模式:一个消息生产者,一个交换机,多个队列,多个消息消费者。此时的自己唯一的Routekey,不是一个确定值,像我们熟悉的正则表达式对应的匹配规则增加topic 通配符 * #
RabbitMQ 使用信道的方式来传输数据。信道(Channel)是生产者、消费者与 RabbitMQ 通信的渠道,信道是建立在 TCP 链接上的虚拟链接。 RabbitMQ 在一条 TCP 链接上建立成百上千个信道来达到多个线程处理,这个 TCP 被多个线程共享,每个信道在 RabbitMQ 都有唯一的 ID,保证了信道私有性,每个信道对应一个线程使用。
拆分多个 queue(消息队列),每个 queue(消息队列) 一个 consumer(消费者),就是多一些 queue (消息队列)而已,确实是麻烦点;或者就一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上 12 点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。
在项目中主要是完成系统之间通信,并且将系统之间的调用进行解耦。例如在添加、修改商品信息后,需要将商品信息同步到索引库、同步缓存中的数据以及生成静态页面一系列操作。在此场景下就可以使用mq。一旦后台对商品信息进行修改后,就向mq发送一条消息,然后通过mq将消息发送给消息的消费端,消费端接收到消息可以进行相应的业务处理。
应用场景:详情页静态化、商品上下架申请、调价申请等。
:
定时任务:在固定时间点执行,或者周期性的去执行某个任务,比如:每天晚上24点做数据汇总,定时发送短信等。
XXL-JOB是一个分布式任务调度平台,有个ADMIN的界面可以管理任务,对任务状态进行修改、启动/停止/终止任务,即时生效。他有两个模块一个是调度中心一个是执行器。
调度中心负责管理调度信息,出调度请求,执行器接收调度中心的请求并执行任务。如果有多个执行器可以采用轮询,随机,故障转移等“路由策略”进行调度。
具体使用的话,把调度中心的jar包上传到服务器,把自带的数据库文件导入生成生成数据库和表,修改一下配置文件然后用命令启动。这时候我们就可以访问调度中心的UI界面了。然后配置执行器在项目里先引入核心的jar包,修改配置文件里的调度中心地址,就可以使用@xxljo注解,调度中心就会显示我们的任务了。
故障转移(FAILOVER)的”路由策略”,当调度中心每次发起调度请求时,会按照顺序对执行器发出心跳检测请求,第一个检测为存活状态的执行器将会被选定并发送调度请求。
为什么用MongoDB不用MySQL
MongoDB是面向文档的非关系型数据库,索引使用B树的数据结构,数据以文件的形式存储在磁盘上。并且MongoDB会将频繁访问的数据缓存在内存中,以提高查询和更新的速度。
MongoDB采用面向文档的存储方式,每个文档都是一个BSON对象,可以包含多个字段,每个字段都有自己的数据类型。
适合存储大数据。它可以处理海量数据,并且可以自动分割数据到多个节点上。
MongoDB使用内存映射技术,将磁盘上的数据映射到内存中,这样可以避免频繁的磁盘访问,提高读取速度。此外,MongoDB还使用了写时复制技术,每个客户端都有一个独立的写副本,这也加快了写入速度。
MongoDB是一种分布式数据库,它可以将数据分布在多个节点上。这样可以减轻单个节点的负载,提高整个系统的处理能力。此外,MongoDB还支持副本集和分片技术,可以提高系统的可用性和可扩展性。
MongoDB怎么使用
连接到MongoDBJava连接Mongodb数据库文章
创建数据库和集合
# 创建集合
collection = db["history"]
# 创建索引以加快查询速度
collection.create_index("timestamp")
插入历史消息数据
# 插入历史消息
message = {
"timestamp": "2022-01-01 10:00:00",
"user": "Alice",
"content": "Hello, World!"
}
collection.insert_one(message)
查询历史消息,查询操作来检索这些数据
# 查询历史消息
query = {"user": "Alice"}
messages = collection.find(query)
for message in messages:
print(message)
更新历史消息
# 更新历史消息
query = {"user": "Alice"}
new_values = {"$set": {"content": "Hi, World!"}}
collection.update_many(query, new_values)
删除历史消息
# 删除历史消息
query = {"user": "Alice"}
collection.delete_many(query)
D:\JAVA传智播客视频\02-ElasticSearch有视频可看