Java基础
面向对象三大特性
OOP,Object Oriented Programming。使用对象来映射现实中的事物,使用对象的关系来描述事物之间的联系。面向对象是相对面向过程而言,面向过程就是分析解决问题所需要的步骤,然后用函数把这些步骤一一实现,使用的时候调用函数。面向对象则是把解决问题按照一定队则划分为多个独立的对象,然后通过调用对象的方法来解决问题。面向对象三大特性:
- 封装:将对象的属性和行为封装起来,不让外界知道具体的实现细节。private修饰的成员变量和方法只能被这个类本身访问;defalult可以被这个类本身和同一包中的类访问;protected可以被这个类本身、他的子类及同一包中的类访问;public可以被所有类访问。
- 继承:基于已有类为基础,构建新的类。子类可以调用父类public、protected以及同包下default的成员变量,还可以重写父类已有的方法。Java中通过super来实现对父类成员的访问,通过this来访问本类的成员。
- 多态:为了解除父子类继承的耦合度,多态允许父类引用指向子类对象(向上转型)。这样父类引用既可以使用子类重写的功能又能访问父类的成员。
类生命周期
java源文件 --javac编译–>
java字节码 --类加载–>
class对象 --实例化–>
实例对象 ----> 卸载
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialication)、使用(Using)和卸载(Unloading)七个阶段。
其中验证、准备和解析三个部分统称为连接(Linking)。加载、验证、准备、解析和初始化是类的加载过程。
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言地运行时绑定(也称动为态绑定)。
加载:读取class文件,将字节流所代表的静态存储结构传化为运行时数据结构存储在方法区内,并在堆中生成该类class类型的对象的过程。
验证:文件格式(是否符合class文件格式规范,并且能够被当前版本虚拟机处理)、元数据(主要对字节码描述的信息进行语义分析)、字节码、符号引用(发生在虚拟 机将符号引用转化为直接引用的时候)
准备:主要为类变量(static)分配内存并设置初始值(数据类型默认值)。这些内存都在方法区分配。
解析:主要是虚拟机将常量池中的符号引用转化为直接引用的过程。
初始化:类构造器()方法执行
类加载
类的加载是将.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆中创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
启动类加载器Bootstrap Classloader 负责虚拟机启动时加载jdk核心类库以及后两个类加载器
扩展类加载器Extension Classloader 继承自ClassLoader,负责加载{JAVA_HOME}/jre/lib/ext/目录下所有的jar包
应用程序类加载器Application Classloader 是Extension ClassLoader的子对象,负责加载应用程序classpath目录下所有的jar和class文件
双亲委派机制
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,子加载器才会尝试执行加载任务。
双亲委派可以避免重复加载,父类已经加载了,子类就不需要再次加载; 更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。
jdk、jre、jvm
JDK是Java开发工具包,包括jre和一些工具javac(编译)、java、javap(反编译)、jconsole(java虚拟机执行状况监视器,用来监控虚拟机内存、线程、cpu使用情况以及相关得java进程MBean)等
JRE是Java运行时环境
JVM是Java Virtual Machine
jdk1.8 新特性
Lambda表达式:匿名函数
Comparator comparator = (x, y) -> Integer.compare(x, y);
Stream API:处理集合数据,可以执行非常复杂的查找、过滤和映射数据等操作。
池
在系统设计中,会使用到”池”的概念。比如数据库连接池,socket连接池,线程池,组件队列。”池”可以节省对象重复创建和初始化所耗费的时间。对那些被系统频繁请求和使用的对象,使用此机制可以提高系统运行性能。
”池”是一种”以空间换时间”的做法,我们在内存中保存一系列整装待命的对象,供人随时差遣。
java创建对象的几种方式
- 使用new语句创建对象,可以调用任意的构造函数。
- 通过反射方式,调用java.lang.Class或者是java.lang.reflect.Constructor类的newInstance()实例方法。
- 调用对象的clone()方法,需要先实现Cloneable接口,实现其中的clone方法。
- 运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。
ObjectOutputStream obj1 = new ObjectOutputStream(new FileOutputStream("a.txt"));
User user1 = new User();
user1.setName("张三");
user1.setAge(20);
obj1.writeObject(user1);
ObjectInputStream obj2 = new ObjectInputStream(new FileInputStream("a.txt"));
User user2 = (User) obj2.readObject();
mybatis缓存机制
Mybatis中有以及缓存和二级缓存,默认情况下一级缓存开启,而且是不能关闭的。
一级缓存
指SqlSession级别的缓存,当同一个SqlSession中进行相同SQL语句查询时,第二次以后的查询不会走数据库查询,而是直接从缓存中获取。
每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,Mybatis根据当前执行语句生成MappedStatement,在LocalCache中进行查询,如果缓存命中,直接返回结果给用户,如果缓存没有命中,查询数据库并将结果写入LocalCache,最后返回结果给用户。
二级缓存
指跨SqlSession的缓存,是mapper级别(namespace)的缓存,对于mapper级别的缓存不同sqlSession是可以共享的。
开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询。
增删改会清空缓存,在CachingExecutor的update()方法里会调用flushCacheIfRequired(ms),flushCacheIfRequired就是从标签中取到的flushCache属性值。增删改的flushCache默认为开启true。
mybatis一级缓存默认开启,二级缓存开启:mybatis.configuration.cache-enabled:true
查询关闭二级缓存
<select id="selectUserById" resultMap="userMap" useCache="false">
二级缓存默认会在inset、update、delete操作后刷新缓存,但也可以手动配置不更新缓存
<update id="updateUserById" parameterType="User" flushCache="false">
java文件拷贝几种方式
- java.lang.io 包下的字节流FileInputStream和FileOutputStream
try(InputStream is = new FileInputStream("src.txt"); OutputStream os = new FileOutputStream("dest.txt", true)) {
byte[] buffer = new byte[1024];
int len = 0;
while((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
} catch(IOException e) {
e.printStackTrace();
}
- 字符流FileReader和FileWriter
FileReader in = new FileReader(new File("s.txt"));
FileWriter out = new FileWriter(new File("t.txt"));
- 字节缓冲流
InputStream in = new BufferedInputStream(new FileInputStream(new File("source.txt")));
OutputStream out = new BufferedOutputStream(new FileOutputStream(new File("target.txt")));
- 字符缓冲流
BufferedReader in = new BufferedReader(new FileReader("s.txt"));
BufferedWriter out = new BufferedWriter(new FileWriter("t.txt"));
- nio通道传输拷贝transferFrom和transferTo
FileInputStream fis = new FileInputStream(new File("s.txt"));
FileOutputStream fos = new FileOutputStream(new File("t.txt"));
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
springboot自动装配
@SpringBootApplication 中@EnableAutoConfiguration
- 依赖组件包含@Configuration配置类,中@Bean注解声明类
- /META-INF下增加spring.factories文件,通过SpringFactoriesLoader来加载
- 获取到配置后通过调用ImportSelector接口来完成动态加载
JVM
JVM
Java Virtual Machine Java虚拟机
Java是一门抽象程度很高的语言,提供了自动内存管理特性。
java具有跨平台语言,一次编译,到处运行。
Java虚拟机主要包括运行时数据区、类加载子系统和字节码执行引擎等。
类加载子系统:负责加载程序中类和接口;
执行引擎:执行字节码文件和本地方法;
Java虚拟机的运行时数据区在内存中,所以这部分也称为JVM内存模型
gc
当新生代Eden区域满时触发minor GC,将Eden和使用的一块survivor区域复制到另一块survivor上,如果一个对象经过了默认15次minor gc(XX:+MaxTenuringThreshold)将会直接进入老年代。
当老年代满时触发major GC(full GC),老年代对象存活时间比较长,因此full GC发生的频率比较低。
JVM调优
JVM调优参数
在JVM中,主要是对堆(新生代)、方法区和栈进行性能调优。各个区域的调优参数如下所示。
堆:-Xms、-Xmx
新生代:-Xmn
方法区(元空间):-XX:MetaspaceSize、-XX:MaxMetaspaceSize
栈(线程):-Xss
-XX:MetaspaceSize: 指的是方法区(元空间)触发Full GC的初始内存大小(方法区没有固定的初始内存大小),以字节为单位,默认为21M。达到设置的值时,会触发Full GC,同时垃圾收集器会对这个值进行修改。
如果在发生Full GC时,回收了大量内存空间,则垃圾收集器会适当降低此值的大小;如果在发生Full GC时,释放的空间比较少,则在不超过设置的-XX:MetaspaceSize值或者在没设置-XX:MetaspaceSize的值时不超过21M,适当提高此值。
-XX:MaxMetaspaceSize: 指的是方法区(元空间)的最大值,默认值为-1,不受堆内存大小限制,此时,只会受限于本地内存大小。
最后需要注意的是: 调整方法区(元空间)的大小会发生Full GC,这种操作的代价是非常昂贵的。如果发现应用在启动的时候发生了Full GC,则很有可能是方法区(元空间)的大小被动态调整了。
所以,为了尽量不让JVM动态调整方法区(元空间)的大小造成频繁的Full GC,一般将-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置成一样的值。例如,物理内存8G,可以将这两个值设置为256M
对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。
1.Full GC
会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。
2.导致Full GC的原因
1)年老代(Tenured)被写满
调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。
2)持久代Pemanet Generation空间不足
增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例
3)System.gc()被显示调用
垃圾回收不要手动触发,尽量依靠JVM自身的机制
- 调优步骤
监控GC状态
生成堆的dump文件
分析dump文件
调整GC类型和内存分配
JVM内存模型
-
虚拟机栈:描述了Java方法执行时的内存模型,即每个方法执行的时候,线程都会在自己的线程栈中同步创建一个栈帧,用于存放局部变量表、操作数栈、动态连接和方法出口等信息。
局部变量表:保存方法参数和局部变量;
操作数栈:
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
每个方法从调用到完成的过程,就对应着一个栈帧在线程栈中从入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError栈溢出异常(单线程独有)
如果虚拟机在动态扩展栈时无法申请到足够的内存空间,抛出OutOfMemoryError异常(多线程会发生)
在Java中,所有的基本数据类型(byte、short、int、long、float、double、boolean、char)和引用变量(对象引用)都是在栈中的。一般情况下,线程退出或者方法退出时,栈中的数据会被自动清除。 -
本地方法栈:与虚拟机栈作用类似,不同的是虚拟机栈为JVM执行的java方法服务,而本地方法栈为JVM调用的本地方法服务。
HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一。
程序计数器:每个线程都会有自己独立的程序计数器,主要功能是记录当前线程执行到哪一行指令了。 -
方法区:保存类型信息(类签名、属性、方法)
存放虚拟机加载的类的元信息、常量池、静态变量等的引用,以及即时编译器编译后的代码缓存等数据。
jdk8后抛弃了永久代的概念,通过在本地内存中实现了元空间代替永久代,并且将常量池和静态变量移到Java堆区。所以方法区是使用直接内存来实现的,这与堆是不一样的,也就是堆和方法区不在同一块物理内存。直接内存并不是JVM运行时数据区的一部分,其分配不会受Java堆大小的限制。 -
堆:存放对象实例,是GC的主要区域。
新生代:Eden区、两个Survivor区,默认比例8:1:1
java虚拟机每次使用新生代中的Eden和其中一块Survivor(From),在经过一次Minor GC(eden区满了)后将Eden和Survivor中还存活的对象一次性复制到另一块Survivor(To)中(复制回收算法),最后清理掉Eden和Survivor(From)空间,此时From和To会互换身份。
将此时Survivor空间还存活的对象年龄设置为1,以后每进行一次GC,他们年龄就增加1,默认到15后,就会把他们移到老年代中。
老年代空间满了会抛出 java.lang.OutOfMemoryError: Java heap space,这是最典型的内存泄漏,简单来说就是堆空间都被无法回收的对象占满了,虚拟机无法再分配新空间。这种情况一般来说是因为内存泄漏或者内存不足造成的。
方法区占满会抛出 java.lang.OutOfMemoryError:PermGen space Perm空间被占满,无法为新的class分配存储空间。这个在java反射大量使用时比较常见,主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。
堆栈溢出:java.lang.StackOverflowError 一般就是递归或者循环调用造成的。
stw:stop-the-world
CMS:
1.初始标记:stw
2.并发标记:三色标记算法
3.重新标记:stw
4.并发清理:
三色标记:
并发标记阶段。
黑色:标记完,孩子标记完;
灰色:自己已经标记完,还没来得及标记fields;
白色:没有标记到的节点;
什么情况会发生Java堆内存溢出?
(java.lang.OutOfMemoryError : Java heap space)
只要不断的创建对象,且GC Roots到对象之间有可达的路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后会出现内存溢出异常。
什么情况会发生Java栈内存溢出?
如果线程请求的栈深度大于虚拟机所允许的最大深度,将会抛出StackOverflowError;如果虚拟机在扩展栈时无法申请到足够的内存空间则会抛出OutOfMemoryError。
容器
集合
-ArrayList中维护一个Object类型数组。当使用无参构造创建时,默认容量为10,扩容时按照当前容量的1.5倍扩容,即 10 --> 15 --> 22 …
Vector 也是List接口一个实现类,是线程安全的,扩容时按2倍扩容。
LinkedList 双向链表,添加和删除效率高。
HashSet底层是HashMap实现的。添加元素时,先通过元素哈希值得到table数组索引。如果该索引位置没有其他元素,直接添加;如果该位置已经有元素,则需要进行equals方法判断,相等则不添加,不等则以链表方式添加。
扩容:第一次添加默认16,临界值为16*0.75=12,0.75为默认加载因子。
如果数组长度到了12,就扩容16 * 2 = 32, 新的临界值为32 * 0.75 = 24。
jdk8中,如果一条链表元素个数到达8,并且table的大小大于等于64,就会转红黑树。否则仍然采用数组扩容机制。
TreeSet底层TreeMap,有序单列集合,需要初始化的时候传入比较器
Set<String> treeSet = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
// lamba表达式写法
Set<String> treeSet = new TreeSet<>((o1, o2) -> o1.compareTo(o2));
Set<String> treeSet = new TreeSet<>(String::compareTo);
HashMap底层是数组+单向链表,1.8后还有红黑树。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果底层table数组为null,或者length为0,就扩容到16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 取出hash值对应的table索引位置的node,如果为null,就直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果链表有元素和准备添加key的hash值相同,且满足key是同一个对象或equals相同
// 就认为是重复key添加
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果是红黑树,就按红黑树方式添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果是链表,就循环比较
else {
for (int binCount = 0; ; ++binCount) {
// 如果整个链表没有和准备添加的相同,就添加到该链表末尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 加入后,判断当前链表个数是否到了8个,到了8个后
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果链表有元素和准备添加key的hash值相同,且满足key是同一个对象或equals相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
Hashtable键和值都不能为null,否则抛出空指针异常;
Hashtable是线程安全的
初始化大小11,临界值threshold为 11 * 0.75 = 8
第一次扩容: 11 << 1 + 1 = 23
public class Properties extends Hashtable<Object,Object> {}
Collections工具类常用方法:
reverse 反转
shuffle 打乱 sort 排序
swap 交换
max 返回最大 min 返回最小
frequency 出现次数
copy 拷贝
replaceAll 替换
Map遍历
- 通过map.keySet
for (String key : map.keySet()) {
System.out.println("key:" + key + " value: " + map.get(key));
}
- 通过map.entrySet使用iterator遍历
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext) {
Map.Entry<String, String> entry = it.next();
System.out.println("key:" + entry.getKey() + " value: " + entry.getValue());
}
- 通过map.entrySet遍历key和value。推荐,容量大时
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key:" + entry.getKey() + " value: " + entry.getValue());
}
- 通过map.values()遍历value,但拿不到key
// Lambda遍历map
map.forEach((k, v) - {
System.out.println(k + ":" + v);
}
// Lambda遍历list
list.stream().forEach(student -> {
if (student.getAge() > 28) {
...
}
}
// list转map
Map<Long, User> userMap = list.stream().collect(Collectors.toMap(User::getId, a -> a,
(oldVal, currVal) -> currVal));
list.stream().filter(student -> {
student.getAge() > 28
}).forEach(System.out.println(student.toString))
Collectors.toMap方法,当出现key重复时,调用合并函数,合并value
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
// list分组
Map<String, List<User>> groupBy = userList.stream().collect(Collectors.groupingBy(User::getName));
// list过滤
List<User> newList = list.stream().filter(a -> a.getId() == 1).collect(Collectors.toList());
int totalAge = list.stream().mapToInt(User::getAge).sum();
网络编程
流关闭
一个流绑定了一个文件句柄(或网络端口),如果流不关闭,该文件(或端口)将始终处于被锁定(不能读取、写入、删除和重命名的)状态,占用大量系统资源却没有释放。
使用try-catch-resources语法创建的资源抛出异常后,JVM会自动调用close 方法进行资源释放,当没有抛出异常正常退出try-block时候也会调用close方法。
使用装饰流时,只需要关闭最后面的装饰流即可
内存流可以不用关闭(关与不关都可以,没影响)
ByteArrayOutputStream和ByteArrayInputStream其实是伪装成流的字节数组(把它们当成字节数据来看就好了),他们不会锁定任何文件句柄和端口,如果不再被使用,字节数组会被垃圾回收掉,所以不需要关闭。
在循环中创建流,需要在循环中关闭流。因为在循环外关闭,关闭的是最后一个流。
web
session和token
- 为什么会有session的出现?
答:是由于网络中http协议造成的,因为http本身是无状态协议,这样,无法确定你的本次请求和上次请求是不是你发送的。
基于session的认证方式
用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id存放到cookie中,这样用户客户端请求时带上session_id就可以验证服务器端是否存在session数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
void setAttribute(String name, Object val);
Object getAttribute(String name);
void removeAttribute(String name); 移除session对象
void invalidate(); 使HttpSession失效 - 为什么会有token的出现?
首先,session的存储是需要空间的;其次,session的传递一般都是通过cookie来传递的。而token在服务器端是可以不需要存储用户的信息的,token的传递方式也不限于cookie传递;当然,token也是可以保存起来的。
基于token的认证方式
用户认证成功后,服务端生成一个token发给客户端,客户端可以放到cookie或者localStorage 等存储中,每次请求时带上token,服务端收到token通过验证后即可确认用户身份。 - token和session的区别?
- token和session其实都是为了身份验证,session一般翻译为会话,而token更多的时候是翻译为令牌;
- session在服务器端会保存一份,可能保存到缓存、文件或数据库;
- session和token都是有过期时间一说,都需要去管理过期时间;
- 其实token与session的问题是一种时间与空间的博弈问题,session是空间换时间,而token是时间换空间。两者的选择要看具体情况而定。
- 虽然确实都是“客户端记录,每次访问携带”,但token很容易设计为自包含的,也就是说,后端不需要记录什么东西,每次一个无状态请求,每次解密验证
,每次当场得出合法/非法的结论。这一切判断依据,除了固化在C/S两端的一些逻辑之外,整个信息是自包含的。这才是真正的无状态。而sessionid,一般都是一段随机字符串,需要到后端去检索id的有效性。万一服务器重启导致内存里的session没了呢?万一redis服务器挂了呢?
————————————————
授权码模式获取token
- 用户访问系统页面
- 判断是否登录
- 跳转认证授权中心/oauth/authorize
- 携带授权码跳转到重定向地址
- 获取token
cookie
Cookie不可跨域名
服务器通过操作Cookie类对象对客户端Cookie进行操作。
通过request.getCookie( ) 获取客户端提交的所有Cookie(以Cookie[ ]数组形式返回)
通过response.addCookie(Cookie cookie)向客户端设置Cookie
Cookie cookie = new Cookie(“username”,“helloweenvsfei”); // 新建Cookie
cookie.setMaxAge(Integer.MAX_VALUE); // 设置生命周期为MAX_VALUE
response.addCookie(cookie); // 输出到客户端
数据库
数据库三大范式
第一范式主要确保数据表中每个字段的值都具有原子性,也就是说表中每个字段不能再被拆分。
在满足第一范式的基础上,还要满足数据库表中的每一条数据,都是可唯一标识的。而且所有非主键字段,都必须完全依赖主键,不能只依赖主键的一部分。
在第二范式的基础上,确保数据表中的每一个非主键字段都和主键字段直接相关,也就是说,要求数据表中的所有非主键字段不能依赖于其他非主键字段字段。
第一范式:确保每列的原子性
第二范式:非主键列完全依赖着主键列
第三范式:非主键列之间不存在依赖关系
范式的目的是为了降低数据的冗余,缺点是可能会降低了查询效率,因为范式等级越高,设计出来的表就越多,越精细,进行查询时就可能需要关联多张表。
实际上设计数据库时,并非会完全遵守这些标准,经常会为了性能违反范式原则,通过增加冗余的数据来提高数据库的性能。
mysql引擎
myisam和innodb这两个引擎,其中最大的区别在于myisam不支持事务,而innodb支持事务。
innodb支持事务,支持行锁,在磁盘上存储的是表空间数据文件和日志文件,使用聚簇索引,索引和数据存在一个文件。
myisam不支持外键,使用非聚簇索引,索引和数据分开,只缓存索引,适合大量查询操作的场景。
myisam保存具体的行数。
myisam索引由B+树构成,执行查询操作的时候会先搜索B+树,如果找到对应叶子结点会,根据叶子节点的值(地址),拿出整行数据。
InnoDB主索引(同时也是数据文件)叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。
隔离级别
Read Uncommited
Read Commited 避免脏读
Repeatable Read mysql默认隔离界别,避免脏读和不可重复读
Serializable 避免幻读,效率低
MVCC
innodb引擎通过MVCC实现了可重复隔离级别,事务开启后,多次执行同样的select快照读,要能读到同样的数据。
MVCC(Multi-Version Concurrency Control)即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。
MVCC使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能。
可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只是锁定必要的行。MVCC会保存某个时间点上的数据快照。这意味着事务可以看到一个一致的数据视图,不管他们需要跑多久。这同时也意味着不同的事务在同一个时间点看到的同一个表的数据可能是不同的。前面说到不同的存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制。
innoDB存储的最基本row中包含一些额外的存储信息 DATA_TRX_ID、DATA_ROLL_PTR、DB_ROW_ID、DELETE BIT。
DATA_TRX_ID标记了最新更新这条行记录的transaction id,每处理一个事务,其值自动+1
DATA_ROLL_PTR 指向当前记录项的rollback segment的undo log记录,找之前版本的数据就是通过这个指针
DB_ROW_ID,当由innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,否则聚集索引中不包括这个值,这个用于索引当中
DELETE BIT位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除,真正意义的删除是在commit的时候。
redo log 主要用于数据库崩溃时的数据恢复
包括存储在内存中的redo log缓冲区 和 存储在磁盘上的redo log文件
写入时机:在完成数据修改后,脏页刷入磁盘之前写入redo log缓冲区。 即先修改再写入。
undo log 确保数据库事务的原子性。redo log记录了事务的行为,很好的保证一致性,对数据进行“重做”操作。但事务有时还需要“回滚”操作,这时就需要undo log。
readView
m_ids: 当前系统中活跃的读写事务id列表
min_trx_id:
max_trx_id:
creator_trx_id:
trx_id == creator_trx_id 可以访问这个版本
trx_id < min_trx_id 可以访问
trx_id > max_trx_id 不可以访问
min_trx_id <= trx_id <= max_trx_id 如果trx_id再m_ids中,不可以访问,反之可以
rc隔离级别每个select语句生成一个readview视图
rr隔离级别一个事务只会生成一个readview视图
redolog
redolog执行流程
- MySQL 客户端将请求语句 update T set a =1 where id =666,发往 MySQL Server 层。
- MySQL Server 层接收到 SQL 请求后,对其进行分析、优化、执行等处理工作,将生成的 SQL 执行计划发到 InnoDB 存储引擎层执行。
- InnoDB 存储引擎层将a修改为1的这个操作记录到内存中。
- 记录到内存以后会修改 redo log 的记录,会在添加一行记录,其内容是需要在哪个数据页上做什么修改。
- 此后,将事务的状态设置为 prepare ,说明已经准备好提交事务了。
- 等到 MySQL Server 层处理完事务以后,会将事务的状态设置为 commit,也就是提交该事务。
- 在收到事务提交的请求以后,redo log 会把刚才写入内存中的操作记录写入到磁盘中,从而完成整个日志的记录过程。
MySQL 将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog,这就是"两阶段提交"。
而两阶段提交就是让这两个状态保持逻辑上的一致。redolog 用于恢复主机故障时的未更新的物理数据,binlog 用于备份操作。两者本身就是两个独立的个体,要想保持一致,就必须使用分布式事务的解决方案来处理。
为什么需要两阶段提交呢?
如果不用两阶段提交的话,可能会出现这样情况
先写 redo log,crash 后 bin log 备份恢复时少了一次更新,与当前数据不一致。
先写 bin log,crash 后,由于 redo log 没写入,事务无效,所以后续 bin log 备份恢复时,数据不一致。
两阶段提交就是为了保证 redo log 和 binlog 数据的安全一致性。只有在这两个日志文件逻辑上高度一致了才能放心的使用。
在恢复数据时,redolog 状态为 commit 则说明 binlog 也成功,直接恢复数据;如果 redolog 是 prepare,则需要查询对应的 binlog事务是否成功,决定是回滚还是执行。
sql一些插入操作
create table users
(
id
int(11) NOT NULL AUTO_INCREMENT,
name
varchar(30) DEFAULT NULL,
PRIMARY KEY (id
),
UNIQUE KEY name_index
(username
)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
插入或更新
insert into user(“id”, “name”, “age”) values (1,‘zhu’,20) ON DUPLICATE KEY UPDATE age = 20;
插入或替换
replace into user (“id”, “name”, “age”) values (NULL, “zhu”, 20);
插入或忽略
insert ignore into user (“id”, “name”, “age”) values (1,‘zhu’,20);
防止重复插入相同记录
insert into user (“id”, “name”, “age”)
select 1, “zhu”, 30 from dual where not exists (select id from user where id = 1);
InnoDB 为什么选B+ 树索引?
- InnoDB 需要执行的场景和功能需要在特定查询上拥有较强的性能。
- CPU 将磁盘上的数据加载到内存中需要花费大量时间。
为什么选择 B+ 树:
3. 哈希索引虽然能提供O(1)复杂度查询,但对范围查询和排序却无法很好的支持,最终会导致全表扫描。
4. B 树能够在非叶子节点存储数据,但会导致在查询连续数据可能带来更多的随机 IO。而 B+ 树的所有叶节点可以通过指针来相互连接,减少顺序遍历带来的随机 IO。
缓存
缓存问题
缓存穿透
数据既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
缓存击穿
缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
缓存雪崩
大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
问题解决
- 缓存穿透:大量请求访问缓存中不存在的key。可以通过业务规则过滤+布隆过滤来解决
业务规则过滤是通过校验key来实现;布隆过滤:如果大量请求访问不存在的key时,先通过布隆过滤检查key在数据库中是否存在,如果存在才允许访问数据库。 - 缓存击穿:大量请求命中某个key,这个key刚好失效。可以通过在查询数据库的时候加锁来解决
- 缓存雪崩:大量请求访问多个key,刚好这些key同时失效。可以通过加锁+key设置不同的失效时间
缓存更新策略
常见的缓存更新策略共有3种:
-
Cache Aside(旁路缓存)策略;
应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。
写策略;先更新数据库,再删除缓存;
读策略:如果命中缓存直接返回,如果没有命中,从数据库读取数据,然后将数据写入到缓存。
Cache Aside 策略适合读多写少的场景,不适合写多的场景,因为当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。如果业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:一种做法是在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了。当然这么做对于写入的性能会有一些影响;
另一种做法同样也是在更新数据时更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快过期,对业务的影响也是可以接受。 -
Read/Write Through(读穿 / 写穿)策略;
-
Write Back(写回)策略;
实际开发中,Redis 和 MySQL 的更新策略用的是 Cache Aside,另外两种策略应用不了。
更新缓存:
1.先写缓存,再写数据库
2.先写数据库,再写缓存
3.先删缓存,再写数据库
4.先写数据库,再删缓存
第一、二种方案在并发场景中,如果多个线程同时执行读写操作,很可能会出现数据不一致问题。
第三种方案先删缓存,再写数据库。在高并发下,也会出现缓存和数据库不一样的情况。比如A执行更新操作,A先删除缓存,在执行更新数据库操作时,另一线程B读取旧数据发现未命中后从数据库读取旧数据,A执行更新操作后就会出现缓存中和数据库不一致现象。
第四种方案也可能出现缓存不一致的问题。比如请求A读数据,请求B更新数据,A读取旧数据在更新缓存之前,B更新了数据并清除缓存,此时A再把读取的旧数据写入缓存,这时就会出现不一致问题。但因为缓存的写入通常要远远快于数据库的写入,所以出现的概率会很小。
可以采用可以“缓存双删”,即在写数据库前删一次,写完后再删一次,第二次删并非立马删,而是间隔一段时间后再删。
先写数据库再删缓存的方案,跟缓存双删的方案一样,有一个共同的风险点,即如果缓存删除失败,也会导致缓存和数据库数据不一样。可以通过加重试机制,可以在更新缓存失败的情况下,重试三次。在接口直接同步重试,如果在该接口并发比较高的时候,可能有点影响接口性能。可以改成异步重试。
异步重试可以通过把重试数据写表,通过定时任务(elastic-job)完成重试 或写入mq等消息中间件,在mq的consumer中处理等。。
使用定时器需要在业务代码中增加额外逻辑,如果删除缓存失败,需要将数据写入重试表;而使用mq方案中,如果删除缓存失败了,需要在业务代码中发送mq消息到mq服务器。
还有一种优雅的实现,通过监听binlog,比如canal等中间件。在业务接口中写数据库后,直接返回成功。mysql服务器会自动把变更的数据写入binlog中,binlog订阅者获取变更的数据,然后删除缓存。
redis
单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。
Redis作为一个成熟的分布式缓存框架,它由很多个模块组成,如网络请求模块、索引模块、存储模块、高可用集群支撑模块、数据操作模块等。
我们所说的Redis单线程,指的是"其网络IO和键值对读写是由一个线程完成的",也就是说,Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。
Redis为什么不需要通过多线程的方式来提升提升I/O的利用率和CPU的利用率?
. Redis 操作基于内存,绝大多数操作的性能瓶颈不在 CPU
. 使用单线程模型,可维护性更高,开发,调试和维护的成本更低
. 单线程模型,避免了线程间切换带来的性能开销
. 在单线程中使用多路复用 I/O技术也能提升Redis的I/O利用率
Linux多路复用技术,就是多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。
Redis为什么性能高
. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
. 数据结构简单,对数据操作也简单,如哈希表、跳表都有很高的性能。
. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU
. 使用多路I/O复用模型
2020年5月份,Redis推出6.0版本,针对处理网络请求过程采用了多线程,而数据的读写命令,仍然是单线程处理的。
读写网络的 read/write 系统调用占用了 Redis执行期间大部分 CPU 时间,瓶颈主要在于网络的 IO 消耗. redis6.0充分利用多核cpu的能力分摊 Redis 同步 IO 读写负荷
redis 为什么这么快
单线程redis吞吐量可达到10w/s
- redis大部分操作都在内存中完成,并且采用了高效的数据结构。因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了。
- redis采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
- redis采用I/O多路复用机制处理大量的客户端socket请求,IO多路复用是指一个线程处理多个io流,就是我们经常听到的select/epoll机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
redis 线程模型
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发生数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)
- 处理关闭文件 2. AOF 刷盘 3. 异步释放 Redis 内存,也就是 lazyfree 线程
redis单线程模式
redis初始化完成后,主线程进入一个事件循环函数。
-
首先调用处理发送队列函数,看发送队列中是否有任务,如果有发送任务,则通过write函数将客户端发送缓存区的数据发送出去,如果这一轮数据没有发生完,就会注册写事件处理函数,等待epoll_wait发现后可写后再处理。
-
调用epoll_wait函数等待事件的到来:
如果是连接事件,则会调用连接事件处理函数,该函数会调用accept获取已连接的socket,接着调用epoll_ctr将已连接的socket加入到epoll,最后注册读事件处理函数。
如果是读事件到来,就会调用事件处理函数,该函数首先调用read获取客户端发送的数据,解析命令、处理命令,将客户端对象添加到发送队列,最后将执行结果写到发送缓存区等待发送。如果是写事件到来,则会调用写事件处理函数,该函数通过write函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发生完,就会继续注册写事件处理函数,等待epoll_wait发现可写后再处理。
redis持久化
redis共有三种数据持久化的方式
AOF日志(Append Only File)
每执行一条写操作,就把该命令以追加的方式写入文件,然后redis重启时,会读取该文件,逐一执行命令的方式恢复数据;
redis追加写数据并不是直接写入硬盘,而是先拷贝到了内核缓冲区page cache,等待内核将数据写入硬盘。redis提供了三种写回硬盘的策略:
- Aways 每次写操作执行完后,同步将AOF日志数据写回硬盘。可靠性最高,最大程度保证了数据不丢失,但性能开销大。
- Everysec 每秒写入,性能适中,宕机时会丢失1s内的数据。
- no 由操作系统自己决定写入时机,性能最高,宕机时丢失的数据可能比较多。
Redis为了避免AOF文件过大,提供了AOF重写机制。当AOF文件大小超过了设定的阈值,Redis就会启用重写机制来压缩AOF文件。重写时,读取数据库中所有键值对,然后将每一个键值对用一条命令记录到新的AOF文件,完成后将新的AOF文件替换掉现有的AOF文件。Redis的重写AOF过程是由后台子进程bgrewriteaof来完成的。
RDB快照
将某一时刻的内存数据,以二进制的方式写入磁盘。
RDB快照恢复数据效率会比AOF高,但redis快照是全量快照,也就是每次执行,都把内存中所有数据都记录到磁盘中,所以快照是一个比较重的操作。
Redis提供了两个命令来生成RDB文件,分别是save和bgsave。save命令是在主线程生成RDB文件,如果写入RDB文件时间太长会阻塞主线程;bgsave是通过创建一个子进程来生成RDB文件,可以通过配置文件来控制每隔一段时间执行一次bgsave命令,默认会提供以下配置:
save 900 1 // 900s内,对数据库进行至少一次修改
save 300 10 // 300s内,对数据库进行至少10次修改
save 60 10000
混合持久方式
集成AOF和RDB优点。
AOF优点是丢失数据少,但数据恢复慢。
RDB优点是数据恢复快,但是快照频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
混合持久化工作在AOF日志重写过程。在AOF重写日志时,fork出来的重写子进程会先将与主线程共享的内存数据以RDB的方式写入到AOF文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以AOF方式写入到AOF文件,写入完成后通知主进程将新的含有RDB格式和AOF格式的AOF文件替换旧的AOF文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。
redis 集群
主从复制、哨兵模式、切片集群。
- 主从复制,一主多从、读写分离。主服务器可进行读写操作,当发生写操作时将写操作同步给从服务器;而从服务器一般只读。
主从服务器间的命令复制是异步,所以无法实现强一致性。 - 哨兵模式,监控主从服务器,提供主从节点故障转移。
- 切片集群。当redis缓存数据量大到一台服务器无法缓存时,就需要redis切片集群,它将数据分布在不同的服务器,以此降低系统对单主节点的依赖,提高redis服务的读写性能。
redis常用命令
连接:redis-cli.exe -h 127.0.0.1 -p 6389
如果配置密码,输入密码登录: auth 123456
quit
keys * # 查看本库所有的键,默认是库0
select 1 # 切换到库1,redis默认有0~15共16个库
flushall 清空数据
赋值与取值
set key value
get key
keys命令
? 匹配一个字符
- 匹配任意个(包括0个)字符
[] 匹配括号间的任一个字符,可以使用 “-” 符号表示一个范围,如 a[b-d] 可以匹配 “ab”,“ac”,“ad”
\x 匹配字符x,用于转义符号,如果要匹配 “?” 就需要使用 ?
exists key # 判断一个key是否存在,存在,返回1,否则返回0
type key # 获得键值的数据类型,返回sting,hash,list,set,zset
incr key # 递增当前key的value,并返回递增后的值,前提是当前value是整数类型;如果当前key不存在,第一次递增后的结果是1
incrby key increment # key的value递增指定的数值
decr key
decrby key increment
append key value # 向键值的末尾追加value,如果键不存在,则将改键的值设置为value,返回value的长度
strlen key # 返回键值的长度,如果键不存在,返回0
mget key1 key2 … # 同时获得多个键值
mset key1 value1 key2 value2 … # 同时设置多个键值
Hash类型常用命令
hset key field value
hget key field
hmset key field1 value1 field2 value2 …
hmget key field1 field2 …
hgetall key
hexists key field # 判断字段是否存在,存在返回1,否则返回0
hsetnx key field value # hsetnx与hset类似,区别在于如果字段已经存在,hsetnx 命令将不执行任何操作
hincrby key field increment # 使字段增加指定的整数
hdel key field1 field2 … # 删除字段,返回被删除的字段个数
hkeys key
hvals key
hlen key # 获取字段数量
List类型常用命令
lpush key value1 value2 … # 向列表左边增加元素,返回表示增加元素后列表的长度
rpush key value1 value2 … # 向列表左边增加元素,返回表示增加元素后列表的长度
lpop key # 从列表左边弹出一个元素,返回该元素
rpop key # 从列表右边弹出一个元素
llen key # 当键不存在时,返回0
lrange key begin end # 获得列表中的某一片段,返回索引从 start 到 stop 之间的所有元素(包括两端的元素) 索引开始为 0
lrem key count value # 删除列表中前 count 个值为 value 的元素,返回值是实际删除的元素个数
lindex key index # 返回指定索引的元素
lset key index value # 设置指定索引元素值
ltrim key start end # 删除指定索引范围之外的所有元素
set集合类型常用命令
sadd key member1 member2 …
srem key member1 member2 …
smembers key # 返回集合中所有元素
sismember key member # 判断一个元素是否在集合中,存在返回1,不存在返回0
sdiff key1 key2 … # 集合间差集
sinter key1 key2 … # 交集
sunion key1 key2 … # 并集
sdiffstore destination key1 key2 … # 同sdiff,区别在于sdiffstore不会直接返回运算的结果,而是将结果存在destination集合中
sinterstore destination key1 key2 …
sunionstore destination key1 key2 …
scard key # 获取集合中元素个数
srandmember key [ count ] # 随机从集合中获取一个元素,或传递count参数指定获得多个元素
spop key # 从集合中随机弹出一个元素
对有序集合sorted set类型常用操作
zadd key score1 member1 score2 member2 … # 向有序集合中加入一个元素和该元素的分数,如果该元素已经存在,则会用新的分数替换原有的分数。返回新加入到集合中的元素个数
redis常用场景
String类型
缓存对象、常规计数、分布式锁、共享session信息等
- 缓存
string类型,热点数据,对象、全页缓存 - 数据共享分布式
string类型,因为redis是分布式的服务,可以在多个应用之间共享。例如分布式session - 分布式锁
string类型setnx方法,只有不存在时才能添加成功,返回true
EXISTS job # job 不存在(integer) 0
SETNX job “programmer” # job 设置成功(integer) 1
SETNX job “code-farmer” # 尝试覆盖 job ,失败(integer) 0 - 全局ID
int类型,incrby,利用原子性
incrby userid 1000
分库分表的场景,一次性拿一段 - 计数器
int类型,incr方法
例如文章阅读量、微博点赞、允许一定的延迟,先写入redis再定时同步到数据库 - 限流
以访问者的ip和其他信息作为key,访问一次增加一次计数,超过次数则返回false
incr uid:123 uid为123的访问+1
get uid:123 获取访问次数 - 位统计
比如获取从1号到3号连续3天登录的uid
10086用户1号登录:setbit onlineuser:1 10086
bittop “and” “onlineuser:1-3” “onlineuser:1” onlineuser:2" “onlineuser:3” - 购物车
string或hash
key:用户id;field:商品id;value:商品数目
List类型
- 消息队列
队列: 先进先出 rpush lpop
阻塞式 rpush blpop
栈: 先进后出 rpush rpop
阻塞式 rpush brpop - 抽奖
spop user_pool 从set中随机抽取一个uid
Set类型
聚合计算(并集、交集、差集)比如点赞、共同关注、抽奖活动等
- 点赞、签到、打卡
sadd like:abc zhu // 点赞
srem like:abc zhu // 取消点赞
sismember like:abc zhu // 是否点赞
smembers like:abc // 点赞所有用户
scard like:abc // 点赞数 - 商品标签
sadd tags:abc abc - 商品筛选
sdiff set1 set2 // 获取差集
sinter set1 set2 // 获取交集
sunion set1 set2 // 获取并集
Zset类型
排序场景,比如排行榜、电话和姓名排序等
- 排行榜
zincrby hotNews:20220308 888 // 为888新闻点击数+1
zrevrange hotNews:20220308 0 15 withscores // 获得当日点击最多15条
redis实现分布式锁
Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:
如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
如果 key 存在,则会显示插入失败,可以用来表示加锁失败。
SET lock_key unique_value NX PX 10000
lock_key 就是 key 键;
unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
优点:
- 性能高效(这是选择缓存实现分布式锁最核心的出发点)。
- 实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。
- 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。
并发编程
线程池
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 空闲线程存活时间单位
BlockingQueue workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler) // 拒绝策略
workQueue工作队列
任务被提交给线程池时,会先进入工作队列,任务调度时再从工作队列中取出。常用工作队列有以下几种
-
ArrayBlockingQueue(数组的有界阻塞队列)
ArrayBlockingQueue 在创建时必须设置大小,按FIFO排序(先进先出)。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。 -
LinkedBlockingQueue(链表的无界阻塞队列)
按 FIFO 排序任务,可以设置容量(有界队列),不设置容量则默认使用 Integer.Max_VALUE 作为容量 (无界队列)。该队列的吞吐量高于 ArrayBlockingQueue。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。有两个快捷创建线程池的工厂方法 Executors.newSingleThreadExecutor、Executors.newFixedThreadPool,使用了这个队列,并且都没有设置容量(无界队列)。
3.SynchronousQueue(一个不缓存任务的阻塞队列)
生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
其 吞 吐 量 通 常 高 于LinkedBlockingQueue。 快捷工厂方法 Executors.newCachedThreadPool 所创建的线程池使用此队列。与前面的队列相比,这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
-
PriorityBlockingQueue(具有优先级的无界阻塞队列)
优先级通过参数Comparator实现。 -
DelayQueue(这是一个无界阻塞延迟队列)
底层基于 PriorityBlockingQueue 实现的,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,而队列头部的元素是过期最快的元素。快捷工厂方法 Executors.newScheduledThreadPool 所创建的线程池使用此队列。
Java中的阻塞队列(BlockingQueue)与普通队列相比,有一个重要的特点:在阻塞队列为空时,会阻塞当前线程的元素获取操作。具体来说,在一个线程从一个空的阻塞队列中取元素时,线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预)。
threadFactory线程工厂
创建一个线程工厂用来创建线程,可以用来设定线程名、是否为daemon线程等等
handler拒绝策略
AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。(默认这种)
DiscardPolicy:丢弃任务,但是不抛出异常
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) 。也就是当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务从队尾添加进去,等待执行。
CallerRunsPolicy:谁调用,谁处理。由调用线程(即提交任务给线程池的线程)处理该任务,如果线程池已经被shutdown则直接丢弃
Executors工具类,不推荐
public static ExecutorService newFixedThreadPool(int nThreads)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ExecutorService newCachedThreadPool()
public static ExecutorService newSingleThreadExecutor()
public static void main(String[] args) {
int corePoolSize = 3;
int maxPoolSize = 5;
long keepAliveTime = 10;
BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>(5);
ThreadFactory factory = (Runnable r) -> {
Thread t = new Thread(r);
t.setDefaultUncaughtExceptionHandler((Thread thread, Throwable e) -> {
System.out.println("factory的exceptionHandler捕捉到异常--->>> \n" + thread.currentThread().getName() + ": " + e.getMessage());
});
return t;
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime,
TimeUnit.SECONDS, queue, factory);
Thread t = new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println(name);
if ("Thread-5".equals(name)) {
throw new RuntimeException("abc");
}
});
IntStream.range(0, 10).forEach(i -> executor.execute(t));
executor.shutdown();
}
控制台输出:
Thread-1
Thread-3
Thread-3
Thread-3
Thread-3
Thread-1
Thread-2
Thread-3
Thread-4
Thread-5
factory的exceptionHandler捕捉到异常--->>>
Thread-5: abc
并发编程三个问题
- 原子性
Java内存模型定义了8中原子操作,此外Java内存模型还保证了对于基本数据类型(char、boolean、int等)的操作是原子性的。对于其他类型的数据如若需要更灵活的原子性操作,Java内存模型提供了lock和unlock操作。JVM中使用的两个字节码指令monitorenter和monitorexit即是通过lock和unlock操作来实现的,常使用的synchronized关键字转换成字节码指令后即由monitorenter和monitorexit构成。 - 可见性
可见性是指当一个线程修改了主内存中变量的值,其他线程可以立即获取这个修改后的新值。只要在工作内存中修改变量之后立即存储到主内存,以及读取一个变量之前强制从主内存刷新变量的值即可保证可见性。volatile关键字即通过上述方法保证多线程操作变量时的可见性。 - 有序性
有序性是指在同一个线程中的所有操作都是有序执行的,但由于指令重排序等行为会导致指令执行的顺序不一定是按照代码中的先后顺序执行的,在多线程中对一个变量的操作就可能会受到指令重排序的影响。volatile关键字包含有禁止指令重排序的作用,因此使用volatile关键字修饰的变量可以保证多线程之间对该变量操作的有序性。
事务四大特性
原子性Automicity:一个事务中的操作,要么全部完成,要么全部不完成;
一致性Consistency:事务开始之前和事务结束后,数据库完整性不会被破坏;
隔离性Isolation:
持久性Duriaility:事务结束后,堆数据的修改是永久的。
锁
每个java对象都在对象头中保存一把锁。
java对象包括对象头、实例数据、填充字节(8bit*n)三部分。
HotSpot对象头包括Mark word 和class point组成。
class point指向当前对象类型所在方法区中的类型数据。
mark word,32bit,存储和当前对象运行时状态有关的数据(对象的HashCode、锁状态标志、指向锁标志的指针、偏向锁id等)。
synchronized通过javac编译生产monitorenter和monitorexit字节码指令,来使线程同步。
jdk1.6后引入了偏向锁、轻量级锁。所以锁共有四种状态,无锁、偏向锁、轻量级锁和重量级锁,锁只能升级不能降级。
无锁:无线程竞争或存在竞争,但以非锁方式同步线程,比如cas,原子操作。
偏向锁:mark word中锁标志位为01,且倒数第三个bit是1,如果为1,代表当前对象的锁状态为偏向锁,否则为无锁。
如果当前状态为偏向锁,再去读mark word的前23个bit,即线程id,通过线程id来确认当前想要获得对象锁的这个线程id是不是老顾客。
假如情况发生变化,不止有一个线程,而是多个线程在竞争锁,那么偏向锁升级为轻量级锁
轻量级锁:当一个线程想要获得某个对象的锁时,假如看到锁标志位为00,那么就知道它是轻量级锁。这时线程会在自己的虚拟机栈中开辟一块被称为LockRecord的空间,存放对象头markword的副本以及owner指针。线程通过cas去尝试获取锁,一旦获得将会复制该对象头中的Markword到LockRecord中,并且将LockRecord中的owner指针指向该对象。对象头中的前30bit将会生成一个指针,指向线程虚拟机栈中的LockRecord,这样就实现了线程和对象锁的绑定。获取了这个对象锁的线程就可以去执行一些任务,这时如果其他线程也想获得该对象,此时其他线程将会自旋等待,不断尝试去看目标对象的锁有没有被释放,如果释放就获取,如果没有就继续循环。一旦自旋等待 的线程超过1个,那么轻量级锁将会升级为重量级锁。
重量级锁:通过monitor来对线程进行控制,完全锁定资源。
悲观锁:坏事一定会发生,所以先上锁;
乐观锁:坏事未必会发生,所以事后补偿;
- 自旋锁(cas):一种常见的乐观锁实现。
ABA问题:加版本号
保障CAS操作的原子性问题(lock指令)
读写锁:
- 读锁:读的时候,不允许写,但允许同时读;
- 写锁:写的时候,不允许写,也不允许读;
排他锁:只有一个线程能访问;
共享锁:可以允许有多个线程访问;
统一锁:大粒度的锁;
分段锁:分成一段一段的锁;
可重入锁:一个线程,如果抢占到了互斥锁的资源,在锁释放之前,再去竞争同一把锁,不需要等待,只需要记录重入次数。
synchronized、reentrantlock(re entrant lock)
主要解决避免死锁的问题,一个已经获得同步锁X的一个线程,在释放X之前再次区竞争锁X的时候,会出现自己等待自己锁释放的情况,就会导致死锁。
happens-before原则
一种内存可见性模型,解决因为指令重排序的存在,导致的数据可见性问题。对于两个操作A和B,这两个操作可以在不同线程中执行。如果A happens-before B,那么可以保证,当A操作执行完后,A操作的执行结果对B操作是可见的。
happens-before只是描述结果的可见,并不表示指令执行的先后顺序,也就是说只要不对结果产生影响,仍然允许指令的一个重排序。
Java 内存模型底层是通过内存屏障(memory barrier)来禁止重排序的。
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
- volatile变量规则:对于一个volatile域的写,happens-before于任意后续对这个volatile域的读
- 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见
- 线程结束规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则
- 中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断
- 对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
- 传递性规则:如果A happens-before B,且B happens-before C, 那么A happens-before C
AQS
使用一个voliate修饰的int类型的同步状态,通过一个FIFO队列完成资源获取的排队工作,把每个参与资源竞争的线程封装成一个Node节点来实现锁的分配。
提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。
线程首先尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个node节点加入到FIFO队列中。接着会不断的循环尝试获取锁,条件是当前节点为head的直接后继才会尝试。如果失败就会阻塞自己直到自己被唤醒。而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。
AQS使用一个int成员变量来表示同步状态,使用Node实现FIFO队列,可以用于构建锁或者其他同步装置
AQS资源共享方式:独占Exclusive(排它锁模式)和共享Share(共享锁模式)
AQS的功能分为两种:独占和共享
独占锁,每次只能有一个线程持有锁。
共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock
获取锁的步骤
- 当一个线程获取锁时,首先判断state状态值是否为0
- 如果state==0,则通过CAS的方式修改为非0状态
- 修改成功,则表明获取锁成功,执行业务代码
修改失败,则把当前线程封装为一个Node节点,加入到队列中并挂起当前线程 - 如果state!=0,则把当前线程封装为一个Node节点,加入到队列中并挂起当前线程
CountDownLatch
同步工具,通过一个计数器来实现,初始值为线程的数量。每当一个线程完成了自己的任务,计数器的值就相应得减1。当计数器到达0时,表示所有得线程都已执行完毕,然后再等待的线程就可以恢复执行任务。
java.util.concurrent.ConcurrentHashMap
1.7 中 segment数组,segment继承ReentrantLock,也就是说每个segment对象就是一把锁,一个segment对象内部存在一个HashEntry数组,也就是说HashEntry数组中数据同步依赖同一把锁。不同HashEntry数组的读写互不干扰,这就是所谓的分段锁。
1.8
java线程模型
Java字节码运行在JVM中,而JVM运行在各个操作系统上,所以当JVM想要进行线程创建和回收的这种操作时,是必须要调用操作系统的相关接口,也就是说JVM线程与操作系统线程之间存在着某种映射关系。这两种不同维度的线程之间的规范和协议呢,就是线程模型。
JVM线程对不同操作系统的原生线程进行了高级抽象,可以使开发者一般情况下可以不用关注下层的细节,而只要专注上层的开发就行了。
在Linux系统中,Linux线程KLT(Kernel Level Thread)又被称为轻量级进程LWP(Light Weight Process)。
线程是抽象概念,因为Linux内核没有专门为线程定义数据结构和调度算法,所以Linux去实现线程的方式是轻量级进程,其实本质还是进程,只不过加了一个轻量级的修饰词。那么轻量级进程与进程之间的区别在哪呢?一个Linux进程拥有自己独立的地址空间,而一个轻量级进程没有自己独立的地址空间,只能共享同一个轻量级进程组下的地址空间。
- 一对一(内核线程模型)
完全依赖操作系统内核提供的内核线程(KLT)来实现多线程,线程的切换调度由系统内核完成,系统内核负责将多个线程执行的任务映射到各个CPU中去执行。
这种线程模型比较简单,可以解决大部分场景下的问题。缺点是用户现场的阻塞和唤醒会直接映射到内核线程,容易引起内核态和用户态的切换,这种频繁切换会降低性能。一些语言引入CAS机制来避免一部分情况下的切换,Java就使用了AQS这种函数级别的锁来减少内核级别的锁,提升性能。 - 多对一(用户线程模型)
即多个用户线程映射到同一个内核线程上,用户线程的创建、调度、同步的所有操作全部都是由用户空间的线程来完成的。
用户线程模型完全建立在用户空间的线程库上,不依赖于系统内核,用户线程的创建、同步、切换和销毁等操作完全在用户态执行,不需要切换到内核态。 - 多对多(混合线程模型)
用户线程仍然在用户态中创建,用户线程的创建、切换和销毁的消耗很低,用户线程的数量不受限制。而LWP在用户线程和内核线程之间充当桥梁,就可以使用操作系统提供的线程调度和处理器映射功能。
当前Java虚拟机使用的线程模型是基于操作系统提供的原生线程模型来实现,Windows系统和Linux系统都是使用的内核线程模型,而Solaris系统支持混合线程模型和内核线程模型两种实现。
java内存模型
java内存模型规定所有成员变量都需要存储在主内存中,线程会在其工作内存中保存需要使用的成员变量的拷贝,线程对成员变量的操作(读取和赋值)都是对其工作内存中的拷贝进行操作。各个线程之间不能访问工作内存,线程变量的传递需要通过主内存来完成。
Java内存模型定义了8种原子操作来实现上图中的线程内存交互:
read,将主内存中的一个变量的值读取出来
load,将read操作读取的变量值存储到工作内存的副本中
use,把工作内存中的变量的值 传递给执行引擎
assign,把从执行引擎中接收的值赋值给工作内存中的变量
store,把工作内存中一个变量的值传递到主内存
write,将store操作传递的值写入到主内存的变量中
lock,将主内存中的一个变量标识为某个线程独占的锁定状态
unlock,将主内存中线程独占的一个变量从锁定状态中释放
设计模式
设计模式原则
- 单一职责原则
对于一个类,只有一个引起该类变化的原因;该类的职责是唯一的,且这个职责是唯一引起其他类变化的原因。 - 接口隔离原则
客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。 - 依赖倒转原则
依赖倒转原则是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。 - 里式代换原则
任何基类可以出现的地方,子类一定可以出现。里氏代换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受影响时,基类才能真正的被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。 - 开闭原则
对于扩展是开放的(Open for extension)
对于修改是关闭的(Closed for modification) - 迪米特法则
迪米特法则又叫做最少知识原则,就是说一个对象应当对其它对象又尽可能少的了解,不和陌生人说话。 - 合成复用原则
合成复用原则要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
创建型模式
对象实例化的模式,创建型模式用于解耦对象的实例化过程。
单例模式
某个类只能有一个实例,提供一个全局的访问点。
双重检查锁,线程安全的单例模式代码实现:
public class LazySingleton{
// 私有静态成员变量,存储唯一实例
private volatile static LazySingleton instance = null;
// 私有构造函数
private LazySingleton(){};
// 公有静态成员方法,返回唯一实例
public static LazySingleton getInstance() {
// 第一次空是为了验证是否创建了对象,判断为了避免不必要的同步
if (instance == null) {
// 锁定代码块
synchronized (LazySingleton.calss) {
// 第二次判空是为了避免重复创建单例,因为可能会存在多个线程通过了第一次判断在等待锁,来创建新的实例对象
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
工厂模式
一个工厂类根据传入的参量决定创建出哪一种产品类的实例。
抽象工厂模式
创建相关或依赖对象的家族,而无需明确指定具体类。
建造者模式
封装一个复杂对象的创建过程,并可以按步骤构造。
原型模式
通过复制现有的实例来创建新的实例,java对象通过实现Cloneable接口来实现复制。
-
浅拷贝
对于数据类型是基本数据类型及string类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。“string”属于Java中的字符串类型,也是一个引用类型,并不属于基本的数据类型。
对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
浅拷贝是使用默认的clone()方法来实现 -
深拷贝
复制对象的所有基本数据类型的成员变量值
为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝
实现方式1:重写clone方法来实现深拷贝
实现方式2:通过对象序列化实现深拷贝(推荐)
结构型模式
关注把现有类或对象结合在一起形成一个更大的结构。
装饰器模式
动态的给对象添加新的功能。
代理模式
为其它对象提供一个代理以便控制这个对象的访问。
桥接模式
将抽象部分和它的实现部分分离,使它们都可以独立的变化。
适配器模式
将一个类的接口转换成客户希望的另一个接口。
适配器模式包括类适配器和对象适配器。在类适配器模式中,适配器和适配者之间是继承(或实现)的关系;在对象适配器模式中,适配器和适配者之间是关联关系。
适配器模式包含三个角色:
– Target(目标抽象类)
– Adapter(适配器类)
– Adaptee(适配者类)
// 类适配器
public class Adapter extends Adaptee implements Target {
public void request() {
super.specificRequest();
}
}
// 对象适配器
public class Adapter extends Target {
private Adaptee adaptee;
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void request() {
adaptee.specificRequest();
}
}
- 优点
– 将目标类和适配者类解耦
– 增加了类的透明性和复用性
– 灵活性和扩展性都非常好 - 缺点
– 增加系统复杂度 - 场景
系统需要使用一些现有的类,而这些类的接口不符合系统需要;适配不同格式数据等。
– spring mvc中的DispatcherServlet类中doDispatch方法中首先根据传入的request来获取对应的handler,然后利用获取的handler获取适配器类adapter,最后利用该适配器调用对应的方法。
– Reader InputStream InputStreamDecode
组合模式
将对象组合成树形结构以表示“部分-整体”的层次结构。
外观模式
对外提供一个统一的方法,来访问子系统中的一群接口。
享元模式
通过共享技术来有效的支持大量细粒度的对象。
行为型模式
类和对象如何交互,及划分责任和算法。
策略模式
定义一系列算法,把他们封装起来,并且使它们可以相互替换。
模板模式
定义一个算法结构,而将一些步骤延迟到子类实现。
命令模式
将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
迭代器模式
一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
四个角色:
- Iterator 抽象迭代器
- ConcreteIterator 具体迭代器
- Aggregate /ˈæɡrɪɡət/ 抽象聚合类
- ConcreteAggregate 具体抽象类
java集合框架中,List和Set都继承自Collection接口,该接口声明如下
public interface Collection<E> extends Iterable<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
...
}
提供了一个iterator()方法,用于返回一个Iterator类型的迭代器对象,用来遍历聚合中的元素。
观察者模式
对象间的一对多的依赖关系。
仲裁者模式
用一个中介对象来封装一系列的对象交互。
备忘录模式
在不破坏封装的前提下,保持对象的内部状态。
解释器模式
给定一个语言,定义它的文法的一种表示,并定义一个解释器。
状态模式
允许一个对象在其对象内部状态改变时改变它的行为。
责任链模式
将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
访问者模式
不改变数据结构的前提下,增加作用于一组对象元素的新功能。
消息队列
rocketmq
使用场景
解耦
异步
流量削峰
数据分发
同时带的问题;
系统可用性降低、复杂性提高、一致性问题
rocketmq角色
NameServer
提供了路由管理、服务注册、服务发现的功能,是一个无状态节点。nameserver是服务发现者,集群中各个角色都需要定时向nameserver上报自己的状态,以便互相发现彼此,超时不上报的话会被从列表中删除。nameserver可以部署多个,当多个namesever存在的时候,其他角色同时向他们上报信息,以保证高可用。nameserver集群间互不通信,没有主备的概念。nameserver内存式存储,nameserver中的broker、topic等信息默认不会持久化。
Broker:消息存储
面向producer和consumer接收和发送消息;向nameserver提交自己的信息;是消息中间件的消息存储、转发服务器;每个broker节点在启动时都会遍历nameserver列表,与每个nameserver建立长连接,注册自己的信息之后定时上报。
broker集群:broker高可用,可以配成Master/Slaver结构,Master可写可读,Slave只可以读,Master将写入的数据同步给Slave。
一个Master可以对应多个slave,但是一个slave只能对应一个master
master与slave的对应关系通过指定相同的brokerName,不同的brokerId来定义。brokerId为0标识Master,非0标识slave;
master多级负载,可以部署多个broker。每个broker与nameserver集群中的所有节点建立长连接,定时注册topic信息到所有nameserver
Producer
消息生产者。通过集群中的其中一个节点建立长连接,获得topic的路由信息,包括topic下面有哪些queue,这些queue分布在哪些broker上等。接下来向提供topic服务的master建立长连接,且定时向master发送心跳。
Consumer
消息消费者。通过nameserver集群获得topic的路由信息,连接到对应的broker上消费消息。
基本概念
主题Topic
分组Group
消息队列Message Queue
偏移量Offset
分类
同步、异步、单向、集群、广播、顺序、延时、批量消息、过滤消息(tag过滤、sql过滤)
消息存储直接保存在磁盘,且采用顺序写,保证消息存储速度;
消息发送使用零拷贝技术
存储设计
Topic(tags,subTopics)
Message(messageId,messageKey)
Queue
Group
Offset
消息并发度:
一个Topic可以分出多个Queue,每一个queue可以存放在不同的硬件上来提高并发。
消息存储结构:
RocketMq消息的存储是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存储文件时CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。
commitLog:存储消息的元数据
consumerQueue:存储消息在commitLog的索引
indexFile:为消息查询提供了一种通过key或者时间区间来查询消息的方法,这种通过indexFile来查找消息的方法不影响消息发送与消费的主流程。
消息刷盘机制
flushDiskType: SYNC_FLUSH/ASYNC_FLUSH
同步刷盘:消息写入磁盘后返回成功状态
异步刷盘:消息被写入内存的pagecache就返回成功状态,当内存消息积累到一定程度,统一触发写磁盘动作,快速写入
高可用机制
broker集群,master和slave
master的brokerId为0,master支持读和写,slave只支持读,也就是producer只能和master的broker连接写入消息,consumer可以连接master和slaver的broker来读取消息。
消息发送高可用:
在创建topic的时候,把topic的多个message queue创建在多个broker组上
消息消费高可用:
当master不可用或者繁忙时,consumer会被自动切换到slave读
消息同步
配置:brokerRole: SYNC_MASTER(同步复制)/ASYNC_MASTER(异步复制)/SLAVE(从节点)
同步复制:master和slave均写成功,才返回成功状态。
异步复制:只要master写成功,即返回成功。异步复制系统有较低的延迟和较高的吞吐,但master出现故障,有些数据没有来得及写入slave,可能导致消息丢失。
通常情况下异步刷盘配合同步复制
负载均衡
producer负载均衡:默认轮训所有message queue发送,让消息平均落在不同的queue上,而queue可以散落在不同的broker上,所以消息就发送到不同的broker上。
consumer负载均衡:
集群模式:每个consumer实例平均分配每个consume queue
广播模式:消费每个queue中的消息,不存在负载均衡。
消息重试
顺序消息重试:当消费失败后,会不断进行消息重试(间隔1s)。这时,会出现消息消费阻塞的情况。
无序消息(普通、定时、延时、事务消息)重试:当消费者消费失败时,可以通过设置返回状态达到消息重试的结果。
无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。
rocketmq默认允许每条消息重试16次,后进入死信消息队列(一个死信队列对应一个Group ID)
消息幂等性
针对重复消息,消费一次和消费多次的结果是一样的。
可以通过添加业务key,消费方保存消费过的消息,通过查询有没有消费过的key,来保证幂等性
也可以根据业务上唯一key对消息做幂等处理
集群部署模式
单master
多master
多master多slave(同步)
多master多slave(异步)
nameserver
主要为消息生产者和消费者提供主题topic的路由信息
topicQueueTable:topic消息队列路由信息,消息发送时根据路由表进行负载均衡
brokerAddrTable:broker基础信息,包括brokerName、所属集群名称、主备broker地址
clusterAddrTable:broker集群信息,存储集群中所有broker名称
brokerLiveTable:broker状态信息,nameserver每次收到心跳包会更新该信息
filterServerTable:broker上的filterServer列表,用于类模式消息过滤
过期文件删除
由于RocketMQ操作CommitLog、ConsumeQueue文件是基于内存映射机制并在启动的时候加载commitLog、ConsumeQueue目录下的所有文件,为了避免内存与磁盘的浪费,不可能将消息永久存储在消息服务器上,所以需要引入过期文件删除机制。
删除过程分别执行清理消息储存文件CommitLog与消息消费队列文件ConsumeQueue,消息消费队列文件与消息存储文件公用一套过期文件机制。
如果非当前写文件在一定时间间隔内没有再次被更新,则认为是过期文件,可以被删除。RocketMQ不会关注这个文件上的消息是否全部被消费。默认每个文件的过期时间为42h,通过Broker配置文件中设置fileReservedTime来改变过期时间。触发文件清除操作是一个定时任务,而且只有定时任务,文件过期删除定时任务默认每10s执行一次。
过期判断
文件保留时间fileReservedTime,也就是最后一个更新时间到现在间隔,如果超过该时间,则认为是过期文件。
此外还有deletePhysicFilesInterval(删除物理文件的时间间隔) 和 destroyMapedFileIntervalForcibly(是否被线程引用)两个配置
删除条件
指定删除文件时间
磁盘空间(DiskSpaceCleanForciblyRatio),默认85
零拷贝Zero-copy技术
是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率。
零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上下文切换而带来的不必要开销。
传统的数据传输机制
buffer = File.read();
Socket.send(buffer);
比如读取文件,再用socket发送出去,实际经过了4次拷贝。
- 将磁盘文件读取到操作系统内核缓冲区(DMA拷贝)
- 将内核缓冲区的数据拷贝到应用程序的缓存(CPU拷贝)
- 将应用程序缓存中的数据拷贝到socket网络发送缓冲区(操作系统内核缓冲区)(CPU拷贝)
4.将socket缓冲区数据拷贝到网卡,由网卡进行网络传输(DMA拷贝)
MMAP内存映射
硬盘上文件位置和应用程序缓冲区进行映射,由于mmap将文件直接映射到了用户空间,所以实际文件读取时根据这个映射关系,直接将文件从磁盘拷贝到用户空间,只进行了一次数据拷贝,不再有文件内从从硬盘拷贝到内核空间的缓冲区。
mmap内存映射:3次拷贝(1次cpu拷贝,2次DMA拷贝)
消息模型Message Model
RocketMQ主要由Producer、Broker、Consumer三部分组成。Producer生产消息,Consumer消费消息,Broker存储消息。
RocketMQ分布式事务
两阶段提交,半事务,执行本地事务,事务回查
确保幂等性,防止消息重复消费
消费失败重试,即使成为死信也需要特殊处理
消息生产的默认选择队列策略:规避策略
消息生产的故障延迟机制策略:轮询+规避
消息零丢失
生产端如何保证发送消息零丢失
- 同步发送+重试
- 事务消息
mq收到消息如何保证消息零丢失
- 同步刷盘
- DLedger主从架构
消费者如何保证消息零丢失
先处理本地事务,再提交offset
如何保证消息的顺序性
RocketMQ本身是支持顺序消息的,它是通过消息组(MessageGroup)保证的,发送顺序消息时需要为每条消息设置归属的消息组,相同消息组的多条消息之间遵循先进先出的顺序关系,不同消息组、无消息组的消息之间不涉及顺序性。
生产者端:
单一生产者,如果是多个生产者即使设置相同的消息组,不同生产者之间产生的消息也无法判定其先后顺序。
串行发送,RocketMQ支持多线程安全访问,但如果生产者使用多线程并行发送,则不同线程间产生的消息无法判定其先后顺序。
消费者端:
单一消费者
串行消费,避免批量消费导致乱序。
保证消息不被重复消费
RocketMQ能够保证消息至少被消费者成功消费一次,正因为这样,MQ就很难避免消息重复消费的问题。
防重问题需要应用本身去实现。
幂等性是指同一个操作在多次执行中产生的结果是相同的。可以通过为每个消息分配一个唯一标识,应用在消费消息时通过唯一标识来判断,如果已经存在则直接跳过。
保证高可用
通过集群部署
多节点多副本部署方式,同步刷盘、同步复制
Spring&Springboot
SpringMVC原理
SpringMVC是Spring的一部分,是基于Java实现的Web MVC框架轻量级、松耦合解决方案。
- 用户发送请求至前端控制器(DispatcherServlet);
- DispatcherServlet接收请求,通过处理器映射器(HandlerMapping)查找匹配的处理器(Handler,即Controller);
- DispatcherServlet将请求交给处理器适配器(HandlerAdapter);
- 处理器适配器执行Handler,也就是执行Controller,返回ModelAndView对象;
- DispatcherServlet将ModelAndView传给视图解析器(ViewResolver)进行解析;
- 视图解析器解析后返回具体的View;
- DispatcherServlet渲染视图(即将模型数据填充至视图中),响应用户。
Mybatis原理
mybatis是一个半ORM对象关系型持久层框架,底层封装了JDBC,支持定制化SQL。
- 读取mybatis-config.xml全局配置文件,配置Mybatis运行环境信息,加载SQL映射文件;
- 构造会话工厂SqlSessionFactory;
- 创建会话对象SqlSession,该对象中包含了执行SQL语句的所有方法;
- Executor执行器操作数据库,它将根据SqlSession传递的参数动态生成SQL语句,同时负责查询缓存的维护;
- Executor接口执行方法中有一个MappedStatement类型参数,该参数是对映射信息的封装;
- 输入参数映射;
- 输出结果映射;
spring事务类型
- 编程式事务:代码耦合度高 手动获取getTransation commit rollback
- 声明式事务:@EnableTransactionManagement开始事务注解支持 需要事务时加@Transaction注解
spring事务三大组件
- PlatformTransactionManager事务处理的核心,定义了事务基本操作方法。
getTransaction
commit
rollback - TransacationDefinition用来描述事务具体规则,即事物的属性。
getIsolationLevel()获取事务隔离级别
getName()获取事务名称
getPropagationBehavior()获取事务传播属性
getTimeout()超时时间
isReadOnly()是否只读 - TransactionStatus获取事务状态
isNewTransaciton()
isRollbackOnly()
isCompleted()
spring事务传播属性
传播性(Propagation propagation() default Propagation.REQUIRED):
-
REQUIRED(默认属性)如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
-
REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
-
NESTED 新建事务,支持当前事,与当前事务同步提交或回滚。
-
MANDATORY 支持当前事务,如果当前没有事务,就抛出异常。
-
NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
-
SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
-
NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:
它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。使用PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA 事务管理器的支持。 使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。
spring mvc原理
首先用户发送请求至前端控制器DispatcherServlet
DispatcherServlet调用处理器映射器HandlerMapping,生成处理器对象返回
DispatcherServlet调用处理器适配器HandlerAdapter,经过适配调用具体的处理器Controller返回ModelAndView
DispatcherServlet将ModelAndView传给视图解析器ViewReslover,返回具体的View
DispatcherServlet根据View渲染视图响应用户
springboot自动装配
实际上就是从spring.facroties文件中获取到对应需要进行自动装配的类,并生成相应的bean对象,然后将它们交给spring容器管理。
@SpringBootApplication=>
@EnableAutoConfiguration=>
@Import({AutoConfigurationImportSelector.class})实现自动装配。
核心方法selectImport,读取META-INF/spring.factories文件,经过去重、过滤返回需要装配的类集合。
log日志等级
fatal > error > warn > info > debug > trace
默认打印info及以上级别日志。
spring ioc
Spring Ioc的对象转换分为以下4个步骤:
Resource -> BeanDefinition -> BeanWrapper -> Object
public interface Resource extends InputStreamSource
Spring可以定义不同类型的bean,最后都可以封装成Resource通过IO流进行读取
Spring可以定义类型的bean对象:
XML:这是Spring最开始定义bean的形式
Annotation :由于通过XML定义bean的繁琐,Spring进行了改进可以通过@Component以及基于它的注解来定义bean。例如:@Service,@Controller等等,它们都可以定义bean ,只不过语义更加明确。
Class:通过@Configuration与@Bean注解定义,@Configuration代理xml资源文件,而@Bean代替标签。
Properties/yml:通过 @EnableConfigurationProperties 与 @ConfigurationProperties 来定义bean。这种形式在Spring boot自动注入里面大量使用。
public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement
Spring通过不同形式来定义bean,最终会把这些定义转化成BeanDefinition 保存在Spring容器当中进行依赖注入。
spring把bean注入ioc容器的方式
- 使用@CompontScan注解扫描声明了@Controller、@Service、@Repository、@Component注解的类;
- 使用@Configuration注解声明配置类,并使用@Bean注解实现Bean的定义,这种方式其实是xml配置方式的一种演变;
- 使用@Import注解,导入配置类或者普通的bean;
- 实现FactoryBean接口,动态构建一个bean实例,SpringCloud OpenFeign里面的动态代理实例就是使用FactoryBean实现的;
- 实现BeanDefinitionRegistryPostProcessor重写postProcessBeanDefinitionRegistry方法,手动向beanDefinitionRegistry中注册了目标类的BeanDefinition
spring依赖注入
场景: UserServiceImpl中注入UserMapper
- 字段注入 @Autowired 默认按照 Bean 类型装配,而 @Resource 默认按照 Bean 的名称进行装配。
@Resource 有两个重要属性:name 和 type。
Spring 将 name 属性解析为 Bean 的实例名称,type 属性解析为 Bean 的实例类型。
如果指定 name 属性,则按实例名称进行装配;
如果指定 type 属性,则按 Bean 类型进行装配;
@Autowired
private UserMapper userMapper;
在容器启动,为对象赋值的时候,遇到@Autowired注解,会用后置处理器机制,来创建属性的实例,然后再利用反射机制,将实例化好的属性,赋值给对象。
- 构造函数
private UserMapper userMapper;
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
lombok注解也是使用构造器注入@RequiredArgsConstructor
private final UserMapper userMapper;
- setter注入
private UserMapper userMapper;
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper
}
spring的BeanFactory和FactoryBean
BeanFactory:The root interface for accessing a Spring bean container.
在spring中,所有的bean都是由BeanFactory(也就是IOC容器)来管理的。
FactoryBean: Interface to be implemented by objects used within a BeanFactory which are themselves factories for individual objects.
生产或者修饰对象生产工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。
springboot全局异常处理
@ControllerAdvice/@RestControllerAdvice 配合 @ExceptionHandler 实现全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常
* @Validated @Valid仅对于表单提交有效,对于以json格式提交将会失效
*/
@ExceptionHandler(BindException.class)
@ResponseBody
public HttpResult BindExceptionHandler(BindException e) {
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
List<String> msgList = new ArrayList<>();
for (ObjectError allError : allErrors) {
msgList.add(allError.getDefaultMessage());
}
return HttpResult.FAIL_BUSINESS_UNAVAILABLE(msgList.toString());
}
/**
* @Validated @Valid 前端提交的方式为json格式
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public HttpResult MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
...
}
}
springboot 预设全局数据
@ControllerAdvice 配合 @ModelAttribute 预设全局数据
@ControllerAdvice
public class MyGlobalHandler {
@ModelAttribute
public void presetParam(Model model){
model.addAttribute("globalAttr","this is a global attribute");
}
}
使用:
public String methodTwo(@ModelAttribute("globalAttr") String globalAttr){
return globalAttr;
}
springboot 请求参数预处理
@ControllerAdvice 配合 @InitBinder 实现对请求参数的预处理
@ControllerAdvice
public class MyGlobalHandler {
@InitBinder
public void processParam(WebDataBinder binder){
/*
* 创建一个字符串微调编辑器
* 参数{boolean emptyAsNull}: 是否把空字符串("")视为 null
*/
StringTrimmerEditor trimmerEditor = new StringTrimmerEditor(true);
/*
* 注册自定义编辑器
* 接受两个参数{Class<?> requiredType, PropertyEditor propertyEditor}
* requiredType:所需处理的类型
* propertyEditor:属性编辑器,StringTrimmerEditor就是 propertyEditor的一个子类
*/
binder.registerCustomEditor(String.class, trimmerEditor);
// 将前台日期格式字符串自动转为 Date类型
binder.registerCustomEditor(Date.class,
new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
}
}
spring security
@Configuration
spring security配置 --> SecurityConfig extends WebSecurityConfigurerAdapter
自定义用户认证逻辑 --> 实现UserDetailsSercice接口loadUserByUsername方法
认证失败处理类 --> 实现AuthenticationEntryPoint接口,commence方法。
token过滤器,验证token有效性 --> 继承OncePerRequestFilter类,重写doFilterInternal方法。
退出逻辑 --> 实现LogoutSuccessHandler接口
采用JWT生成token是因为它轻量级,无需存储可以减小服务端的存储压力。但是,为了实现退出功能,不得不将它存储到Redis中
JWT是 json web token 缩写。它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证 token的正确性,只要正确即通过验证。
传统的身份鉴定的方法是在服务端存储一个session,给客户端返回一个cookie,而使用JWT之后,当用户使用它的认证信息登陆系统之后,会返回给用户一个JWT,用户只需要本地保存该token(通常使用local storage,也可以使用cookie)即可。
当用户希望访问一个受保护的路由或者资源的时候,通常应该在 Authorization头部使用 Bearer 模式添加JWT,其内容看起来是下面这样:
Authorization: Bearer
因为用户的状态在服务端的内存中是不存储的,所以这是一种 无状态 的认证机制。服务端的保护路由将会检查请求头 Authorization 中的JWT信息,如果合法,则允许用户的行为。由于JWT是自包含的,因此减少了需要查询数据库的需要。
JWT的这些特性使得我们可以完全依赖其无状态的特性提供数据API服务,甚至是创建一个下载流服务。因为JWT并不使用Cookie的,所以你可以使用任何域名提供你的API服务而不需要担心跨域资源共享问题(CORS)。
- 利用Ant表达式实现权限控制;
http.authorizeRequests()
.antMatchers(“/admin/“).hasRole(“ADMIN”)
.antMatchers(”/user/”).hasRole(“USER”)
.antMatchers(“/visitor/**”).permitAll()
.anyRequest().authenticated() // 除上面外的所有请求全部需要鉴权认证
.and()
.formLogin().permitAll()
.and().csrf().disable();
2.利用授权注解结合SpEl表达式实现权限控制;
@PreAuthorize:方法执行前进行权限检查;
@PreAuthorize(“hasRole(‘ADMIN’)”) // 用户必须具备 admin 角色
@PreAuthorize(“#age>100”) // age 参数必须大于 100
@PreAuthorize(“principal.username.equals(‘javaboy’)”) // 只有当前登录用户名为 javaboy 的用户才可以访问该方法
@PostAuthorize:方法执行后进行权限检查;
@Secured:类似于 @PreAuthorize。
3.利用过滤器注解实现权限控制;
@PreFilter和@PostFilter,这两个注解可以对集合类型的参数或返回值进行过滤。
@PostFilter(“filterObject.id%20") // 只返回结果中id为偶数的user元素。
// filterObject是@PreFilter和@PostFilter中的一个内置表达式,表示集合中的当前对象。
@PreFilter(filterTarget = “ages”,value = "filterObject%20”)
public void getAllAge(List ages,List users) {…} // filterTarget 指定过滤对象 - 利用动态权限实现权限控制。
授权(RBAC实现授权)
RBAC 基于角色的访问控制(Role-Based Access Control)是按角色进行授权,当需要修改角色的权限的时候就需要修改授权的相关代码,系统可扩展性差。
if(主体.hasRole(“总经理角色id”)|| 主体.hasRole(“部门经理角色id”)){
查询工资
}
RBAC 基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,系统设计时定义好查询工资的权限标识,机试查询工资所需的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强。
if(主体.hasPermission(“查询工资的权限标识”)){
查询工资
}
cors跨域
Cross-origin resource sharing 跨域资源共享。
同源:协议、域名、端口号都相同。浏览器同源策略,是浏览器最核心也最基本的安全功能。
cors允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
浏览器端:
目前,所有浏览器都支持该功能(IE10以下不行)。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。
服务端:
CORS通信与AJAX没有任何差别,因此不需要改变以前的业务逻辑。浏览器会在请求中携带一些头信息,以此判断是否运行其跨域,然后在响应头中加入一些信息即可。
可以通过重写corsFilter或重写WebMvcConfigurer
csrf跨站点请求伪造
Cross Site Request Forgery 跨站点请求伪造。
CSRF攻击是源于Web的隐式身份验证机制!Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。CSRF攻击的一般是由服务端解决。
spring security 默认开启csrf,过滤器CsrfFilter来判断是不是
oauth2
OAuth 的核心就是向第三方应用颁发令牌。
OAuth 2.0 规定了四种获得令牌的流程,向第三方应用颁发令牌。
授权码(authorization-code)
隐藏式(implicit)
密码式(password):
客户端凭证(client credentials)
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
授权服务器:客户端注册、客户端授权、为获得授权的客户端颁发令牌、颁发刷新令牌并响应令牌刷新请求
1.创建oauth2认证服务
@EnableAuthorizationServer注解标注认证服务
@EnableWebSecurity注解标注Security认证配置
2.创建oauth2资源服务
@EnableResourceServer注解标注资源服务
mybatis返回自增列
-
@Insert(“insert into attachment (name) values (#{name})”)
@Options(useGeneratedKeys = true, keyProperty = “id”, keyColumn = “id”)
int insert(Attach attach); - insert into attachment (name) values (#{name}) select LAST_INSERT_ID()
- insert into attachment (name) values (#{name}) select @@identity
- insert into attachment (name) values (#{name})
- insert into attachment set name = (#{name})
-
批量返回自增主键
INSERT INTO user(name,pwd) VALUES
(#{u.name})
int i = attachMapper.insert(attach);
新增条数:i, 返回自增主键:attach.getId();
mybatis中${}和#{}占位符区别
#和$都是实现动态sql的方式
#号占位符等同于JDBC里面一个?号占位符,它相当于向PreparedStatement里面预处理语句设置参数,而PreparedStatement里面的sql是预编译的,
$占位符相当于在传递参数的时候,直接把参数拼接到了原始sql里面,mybatis不会对它进行特殊处理。
分布式
CAP
一致性(Consistency):所有节点在同一时间具有相同的数据;
可用性(Availability) :保证每个请求不管成功或者失败都有响应;
分隔容忍(Partition tolerance) :系统中任意信息的丢失或失败不会影响系统的继续运作。
什么是分布式
分布式是一个概念,是为了解决单个物理服务器容量和性能瓶颈问题而采用的优化手段。该领域涉及的问题比较多,如分布式锁、分布式缓存、分布式事务、分布式文件系统、分布式数据库等。
从理念上来说,分布式的实现方式有两种:
水平扩展:当一台机器扛不住流量时,就通过添加机器的方式,将流量平分到所有服务器上,所有机器都可以提供相当的服务;
垂直拆分:单一应用根据业务功能对系统进行拆分。
分布式事务
分布式系统中实现事务,它是由多个本地事务组合而成。
2阶段提交(Two-phase commit protocol)/XA
准备阶段和提交阶段。是一种强一致性设计,引入了一个事务协调者角色来协调管理各参与者的提交和回滚。
两阶段提交,对业务侵入很小,最大优势是对使用方透明,用户可以像本地事务一样使用基于XA协议的分布式事务,能够严格保证事务ACID特性。
缺点也很明显,它是一个强一致性的同步阻塞协议,事务执行过程中需要将所需资源全部锁定,也就是俗称的刚性事务。所以它比较适用于执行时间确定的短事务,整体性能比较差。一旦事务协调者宕机或者发生网络抖动,会让事务参与者一直处于锁定资源的状态或者只有一部分参与者提交成功,导致数据不一致。因此在高并发性能至上的场景中,基于XA协议的分布式事务并不是最佳选择。
3阶段提交
相比于2PC,它在参与者中引入了超时机制,并且新增了一个预提交阶段,使得参与者可以统一各自状态。
TCC(Try-Confirm-Cancel)
2PC和3PC都是数据库层面的,而TCC是业务层面的分布式事务。
Try指的是预留,即资源的预留和锁定;Confirm指的是确认操作,这一步其实就是真正的执行;Cancel指的是撤销操作。
TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。还有一点要注意,撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等。
相对于 2PC、3PC ,TCC适用的范围更大,但是开发量也更大,毕竟都在业务上实现,而且有时候你会发现这三个方法还真不好写。不过也因为是在业务上实现的,所以TCC可以跨数据库、跨不同的业务系统来实现事务。
消息事务
消息事务其实就是基于消息中间件的两阶段提交,将本地事务和发消息放在同一个事务里,保证本地操作和发送消息同时成功。
基于消息中间件的两阶段提交方案,通常用在高并发场景下使用,牺牲数据的强一致性换取性能的大幅提升,不过实现这种方式的成本和复杂度是比较高的,还要看实际业务情况
中间件Seata
Seata的设计目标是对业务无侵入,因此从业务无侵入的2PC方案着手,在传统2PC的基础上演进。它把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务。
也是两阶段提交演变而来的一种分布式事务解决方案,提供了AT、TCC、SAGA和XA等事务模型。
Seata 是一个需独立部署的中间件,所以先搭 Seata Server。
Seata 对业务代码的侵入性非常小,代码中使用只需用 @GlobalTransactional 注解开启一个全局事务即可
分布式锁
分布式锁是一种跨进程、跨机器节点的一种互斥锁。它用来保证在多个机器节点对共享资源访问的排他性。
线程锁的声明周期是单进程多线程,分布式锁是多进程多机器节点。
需要满足锁的排他性、可重入性,需要有锁的获取和释放方法以及锁的失效机制(避免死锁)。
实现方案
- 基于数据库唯一索引
加锁时我们在数据库中插入一条锁记录,利用业务id进行防重。当第一个竞争者加锁成功后,第二个竞争者再来加锁就会抛出唯一索引冲突,如果抛出这个异常,我们就判定当前竞争者加锁失败。防重业务id需要我们自己来定义,例如我们的锁对象是一个方法,则我们的业务防重id就是这个方法的名字,如果锁定的对象是一个类,则业务防重id就是这个类名。 - 基于redis缓存
理论上来说使用缓存来实现分布式锁的效率最高,加锁速度最快,因为Redis几乎都是纯内存操作,而基于数据库的方案和基于Zookeeper的方案都会涉及到磁盘文件IO,效率相对低下。一般使用Redis来实现分布式锁都是利用Redis的SETNX key value这个命令,只有当key不存在时才会执行成功,如果key已经存在则命令执行失败。 - 基于Zookeeper
Zookeeper一般用作配置中心,其实现分布式锁的原理和Redis类似,我们在Zookeeper中创建瞬时节点,利用节点不能重复创建的特性来保证排他性。
redission内置了watch dog的机制来对锁续期
在redis在搭建高可用集群情况下,会出现主从切换导致key失效,也可导致多个进程或线程抢占同一个锁资源的情况。Redis官方提供了RedLock的解决办法。
分布式锁他可以理解为一个cp模型,而redis是一个ap模型。所以在集群模式下,由于数据一致性导致的极端情况下,多线程或多进程抢占锁的情况很难避免。基于cp模型,实现分布式锁还可以选择zookeeper或者etcd,在数据一致性方面,zk用到了zab协议保证数据一致性,而etcd用到了raft算法保证数据的一致性;在锁的互斥方面,zk可以基于有序节点结合watch机制去实现互斥和唤醒,而etcd可以基于prefix机制和watch机制去实现互斥和唤醒。
分段锁,类似concurrentHashmap分段锁,提升性能
主从问题,解决->redlock 奇数redis集群
服务高可用
高可用描述的是一个系统大部分时间都是可用的,判断标准一般是几个9。
而可用性为99.99%的系统全年不可用时间为53分钟;至于99.999%的系统全年不可用时间仅仅约为5分钟。目前大部分企业的高可用目标是4个9,就是99.99%,也就是允许系统的年不可用时间约为53分钟
- 导致系统不可用的情况:
1.硬件故障,比如服务器宕机;
2.并发量/用户请求激增导致整个服务宕掉或者部分服务不可用;
3.代码问题; - 提高系统高可用的方法:
1.使用集群,冗余备份,减少单点故障;
2.注重代码质量,定时review代码;
3.限流,流量控制;
4.使用缓存;
5.异步调用;
6.超时和重试机制;
7.熔断机制;
保证幂等性
- 非高并发场景可以先select,如果存在进行update,否则执行insert;
- 加悲观锁,通过select … for update 加行锁
- 乐观锁,加version update tab set …,version = version + 1 where … and version = 1;
- 通过数据库唯一索引
- 防重表(id和唯一索引字段,唯一索引字段可以是code+name)
- 状态机,根据业务表状态,且状态有规律,按照业务节点从小到大(比如 1-下单、2-已支付、3-完成、4-撤销)
- 加分布式锁,加唯一索引和防重表本质是使用了数据库的分布式锁,但数据库分布式锁性不好用,可以使用redis或者zk
秒杀
瞬时高并发
-
页面静态化,以及秒杀按钮(通过js控制,到了秒杀时间才可用)
-
CDN加速(Content Delivery,内容分发网络)用户就近获取所需内容,降低网络拥堵,提高用户访问响应速度
提前将css、js和图片等静态文件资源缓存到CDN上 -
缓存redis,先查缓存是否有库存,没有直接返回
a. 缓存穿透问题
加锁,影响性能,可以使用布隆过滤器,先从布隆过滤器查该商品是否存在,如果存在才允许从缓存中查询。但还需要考虑缓存和过滤器中数据同步,适合缓存数据更新很少的场景。如果更新操作频繁,可以将不存在的商品id也缓存起来,超时时间设置尽量短一点。
b. 缓存击穿问题
缓存预热,提前把秒杀商品放入缓存 -
mq异步处理
秒杀、下单、支付操作的异步,秒杀成功,发送mq消息到mq服务器,通过mq消息消费执行下单操作
消息丢失问题可以加一张消息发送表;重复消费问题可以加一张消息处理表 -
限流
基于同一用户限流
基于同一ip限流
基于接口限流
加验证码 -
分布式锁
a. 加锁
String result = jedis.set(lockKey, requestId, "NX", "PX", exprieTime);
if ("OK".equals(result)) {
return true;
}
return false;
b. 释放锁
if (jedis.get(lockKey).equals(requestId)) {
jedis.del(lockKey);
return true;
}
return false;
自旋锁
try {
Long start = System.currentTimeMillis();
while(true) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
Long time = System.currentTimeMills();
if (time >= timeOut) {
return false;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
unlock(lockKey, requestId);
}
return false;
使用redis还有锁竞争、续期、锁重入及多个redis实例加锁问题,可以使用redisson
扣减库存
- 防超卖,在update之前先查下库存是否足够。
- update和查操作的原子性,基于数据库乐观锁
update product set stock = stock - 1 where id = productId and stock > 0; - 数据库乐观锁高并发场景并不适用,可以使用redis的原子性方法incr
// 1. 判断用户是否参与过秒杀
boolean exist = redisClient.query(productId, userId);
if (exist) {
return -1;
}
// 2. 查询库存
int stock = redisClient.queryStock(productId);
if (stock <= 0) {
return 0;
}
// 3. 扣减库存
redisClient.incrby(productId, -1);
// 4. 保存秒杀记录
redisClient.add(productId, userId);
return 1;
保证2,3操作原子性
if (redisClient.incrby(productId, -1) < 0) {
return 0;
}
但高并发多个用户同时扣减,还是会出现库存为负,可以使用lua脚本扣减库存
StringBuilder lua = new StringBuilder();
lua.append("if (redis.call('exists', KEYS[1]) == 1) then");
lua.append(" local stock = tonumber(redis.call('get', KEYS[1]));");
lua.append(" if (stock == -1) then");
lua.append(" return 1;");
lua.append(" end;");
lua.append(" if (stock > 0) then");
lua.append(" redis.call('incrby', KEYS[1], -1);");
lua.append(" return stock;");
lua.append(" end;");
lua.append(" return 0;");
lua.append("end;");
lua.append("return -1;");
微服务(Spring Cloud Alibaba)
服务治理
服务的注册、发现以及剔除
服务调用
基于Http的restful 和基于tcp的RPC远程调用
Feign 组件
负载均衡
服务端 nginx、客户端
ribbon组件
@LoadBalanced
策略: 轮询、随机、
服务网关
统一入口
服务熔断
防止服务雪崩
Hystrix
Sentinel
- 流量控制
- 熔断降级
对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
Sentinel和Hystrix原则是一致的,都是当一个资源出现问题时,让其快速失败,不涉及到其他服务。但在限制手段上,Hystrix采用的是线程池隔离方式,优点是做到了资源之间的隔离,缺点是增加了线程切换的成本。Sentinel采用的是通过并发线程的数量和响应时间来对资源做限制。
Sentinel熔断策略:
慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。
异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。 - 系统负载保护
链路追踪
服务器
CPU资源占用高
产生原因
1、代码中存在死循环
2、定时任务跑批量
3、tomcat高并发项目的时候,所有线程都处在运行状态,消耗CPU资源
4、分布式锁的重试机制
a、乐观锁:能够保证用户线程一直在用户态,缺点是消耗CPU的资源
b、CAS自旋锁
解决
- top查看cpu使用情况,定位pid进程
- jstack pid查看栈信息输出
- 定位哪一个线程占用率高
ps -mp pid -o THREAD,tid,time
ps -Lfp pid
top -H ----直接查看高CPU的线程 - 将线程ID转为16进制
- 通过16进制的线程id在栈信息中定位代码行
正向代理和反向代理
正向代理是客户端的代理,服务器不知道真正的客户端是谁;
反向代理是服务器的代理,客户端不知道真正的服务器是谁
常见负载均衡算法
- 随机
- 轮询
- 源地址哈希法
通过对发送请求客户端ip地址进行求hash值,并对服务器地址列表长度取余,选择结果对应的服务器。该方法保证同一个客户端ip地址会被映射到相同的后端服务器,可以保证服务消费者和服务提供者之间建立有状态的session会话。 - 加权轮询法
给每个服务器都设置权重,配置低、负载高的服务器权重低,配置高、负载低的服务器权重高。让权重高的服务器接收到请求的概率更高。 - 最小连接算法
前面几个算法基本只考虑了请求数上的负载均衡,而没有考虑到每个请求处理时长。最小连接算法根据每个服务器当前连接的请求来选择连接请求数最小的服务器,因此该算法需要为每个服务器地址维护一个连接数变量来记录当前服务器连接的请求数。 - 加权随机法
与加权轮询法类似,加权随机法也是根据后端服务器不同的配置和负载情况来配置不同的权重。不同的是,它按照权重来随机选择服务器,而不是顺序。
Linux常用命令
cd ls mkdir rm mv cp
find grep more less head tail
chmod chown
tar -cvf 打包 tar -xvf 解压 zip
yum rpm apt-get
ps -ef
jsp 显示当前java进程pid
df -h 查看磁盘信息
top 动态显示当前耗费资源最多的进程信息
top -Hp pid 查看指定进程下线程信息
tree
system
ifconfig 查看网络情况
ping 测试网络联通
telnet ip port : 查看某一个机器上的某一个端口是否可以访问,如:telnet 197.0.35.1 8080
netstat 显示网络状态信息
kill 杀死进程