OOP,Object Oriented Programming。使用对象来映射现实中的事物,使用对象的关系来描述事物之间的联系。面向对象是相对面向过程而言,面向过程就是分析解决问题所需要的步骤,然后用函数把这些步骤一一实现,使用的时候调用函数。面向对象则是把解决问题按照一定队则划分为多个独立的对象,然后通过调用对象的方法来解决问题。面向对象三大特性:
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是Java开发工具包,包括jre和一些工具javac(编译)、java、javap(反编译)、jconsole(java虚拟机执行状况监视器,用来监控虚拟机内存、线程、cpu使用情况以及相关得java进程MBean)等
JRE是Java运行时环境
JVM是Java Virtual Machine
Lambda表达式:匿名函数
Comparator comparator = (x, y) -> Integer.compare(x, y);
Stream API:处理集合数据,可以执行非常复杂的查找、过滤和映射数据等操作。
在系统设计中,会使用到”池”的概念。比如数据库连接池,socket连接池,线程池,组件队列。”池”可以节省对象重复创建和初始化所耗费的时间。对那些被系统频繁请求和使用的对象,使用此机制可以提高系统运行性能。
”池”是一种”以空间换时间”的做法,我们在内存中保存一系列整装待命的对象,供人随时差遣。
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();
一级缓存:SqlSession级别缓存,默认开启,是一种内存型缓存,不要求实体对象实现Serializable接口。
二级缓存:namespace级别缓存,可以是由一个SqlSessionFactory创建的SqlSession之间共享缓存数据。要求实体类必须实现序列化接口。
二级缓存与一级缓存原理相同,默认也是采用PerpetualCache,HashMap存储,不同在于其存储作用域为Namespace,并且可以自定义存储源。
sqlsession执行查询,首先会查询二级缓存,二级缓存中没有再去查询一级缓存,一级缓存中没有再从数据库中查找,并将结果放到缓存。
增删改会清空缓存,在CachingExecutor的update()方法里会调用flushCacheIfRequired(ms),flushCacheIfRequired就是从标签中取到的flushCache属性值。增删改的flushCache默认为开启true。
mybatis一级缓存默认开启,二级缓存开启:mybatis.configuration.cache-enabled:true
查询关闭一级缓存
二级缓存默认会在inset、update、delete操作后刷新缓存,但也可以手动配置不更新缓存
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 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"));
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);
@SpringBootApplication 中@EnableAutoConfiguration
Java Virtual Machine Java虚拟机
Java是一门抽象程度很高的语言,提供了自动内存管理特性。
java具有跨平台语言,一次编译,到处运行。
Java虚拟机主要包括运行时数据区、类加载子系统和字节码执行引擎等。
类加载子系统:负责加载程序中类和接口;
执行引擎:执行字节码文件和本地方法;
Java虚拟机的运行时数据区在内存中,所以这部分也称为JVM内存模型
当新生代Eden区域满时触发minor GC,将Eden和使用的一块survivor区域复制到另一块survivor上,如果一个对象经过了默认15次minor gc(XX:+MaxTenuringThreshold)将会直接进入老年代。
当老年代满时触发major GC(full GC),老年代对象存活时间比较长,因此full GC发生的频率比较低。
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自身的机制
虚拟机栈:描述了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.lang.OutOfMemoryError : Java heap space)
只要不断的创建对象,且GC Roots到对象之间有可达的路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后会出现内存溢出异常。
如果线程请求的栈深度大于虚拟机所允许的最大深度,将会抛出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
Collections工具类常用方法:
reverse 反转
shuffle 打乱 sort 排序
swap 交换
max 返回最大 min 返回最小
frequency 出现次数
copy 拷贝
replaceAll 替换
for (String key : map.keySet()) {
System.out.println("key:" + key + " value: " + map.get(key));
}
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());
}
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key:" + entry.getKey() + " value: " + entry.getValue());
}
// 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其实是伪装成流的字节数组(把它们当成字节数据来看就好了),他们不会锁定任何文件句柄和端口,如果不再被使用,字节数组会被垃圾回收掉,所以不需要关闭。
在循环中创建流,需要在循环中关闭流。因为在循环外关闭,关闭的是最后一个流。
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); // 输出到客户端
第一范式主要确保数据表中每个字段的值都具有原子性,也就是说表中每个字段不能再被拆分。
在满足第一范式的基础上,还要满足数据库表中的每一条数据,都是可唯一标识的。而且所有非主键字段,都必须完全依赖主键,不能只依赖主键的一部分。
在第二范式的基础上,确保数据表中的每一个非主键字段都和主键字段直接相关,也就是说,要求数据表中的所有非主键字段不能依赖于其他非主键字段字段。
第一范式:确保每列的原子性
第二范式:非主键列完全依赖着主键列
第三范式:非主键列之间不存在依赖关系
范式的目的是为了降低数据的冗余,缺点是可能会降低了查询效率,因为范式等级越高,设计出来的表就越多,越精细,进行查询时就可能需要关联多张表。
实际上设计数据库时,并非会完全遵守这些标准,经常会为了性能违反范式原则,通过增加冗余的数据来提高数据库的性能。
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 避免幻读,效率低
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视图
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事务是否成功,决定是回滚还是执行。
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);
为什么选择 B+ 树:
3. 哈希索引虽然能提供O(1)复杂度查询,但对范围查询和排序却无法很好的支持,最终会导致全表扫描。
4. B 树能够在非叶子节点存储数据,但会导致在查询连续数据可能带来更多的随机 IO。而 B+ 树的所有叶节点可以通过指针来相互连接,减少顺序遍历带来的随机 IO。
数据既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
常见的缓存更新策略共有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 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。
Redis作为一个成熟的分布式缓存框架,它由很多个模块组成,如网络请求模块、索引模块、存储模块、高可用集群支撑模块、数据操作模块等。
我们所说的Redis单线程,指的是"其网络IO和键值对读写是由一个线程完成的",也就是说,Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。
. Redis 操作基于内存,绝大多数操作的性能瓶颈不在 CPU
. 使用单线程模型,可维护性更高,开发,调试和维护的成本更低
. 单线程模型,避免了线程间切换带来的性能开销
. 在单线程中使用多路复用 I/O技术也能提升Redis的I/O利用率
Linux多路复用技术,就是多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。
. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
. 数据结构简单,对数据操作也简单,如哈希表、跳表都有很高的性能。
. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU
. 使用多路I/O复用模型
2020年5月份,Redis推出6.0版本,针对处理网络请求过程采用了多线程,而数据的读写命令,仍然是单线程处理的。
读写网络的 read/write 系统调用占用了 Redis执行期间大部分 CPU 时间,瓶颈主要在于网络的 IO 消耗. redis6.0充分利用多核cpu的能力分摊 Redis 同步 IO 读写负荷
单线程redis吞吐量可达到10w/s
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发生数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)
redis初始化完成后,主线程进入一个事件循环函数。
首先调用处理发送队列函数,看发送队列中是否有任务,如果有发送任务,则通过write函数将客户端发送缓存区的数据发送出去,如果这一轮数据没有发生完,就会注册写事件处理函数,等待epoll_wait发现后可写后再处理。
调用epoll_wait函数等待事件的到来:
如果是连接事件,则会调用连接事件处理函数,该函数会调用accept获取已连接的socket,接着调用epoll_ctr将已连接的socket加入到epoll,最后注册读事件处理函数。
如果是读事件到来,就会调用事件处理函数,该函数首先调用read获取客户端发送的数据,解析命令、处理命令,将客户端对象添加到发送队列,最后将执行结果写到发送缓存区等待发送。
如果是写事件到来,则会调用写事件处理函数,该函数通过write函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发生完,就会继续注册写事件处理函数,等待epoll_wait发现可写后再处理。
redis共有三种数据持久化的方式
每执行一条写操作,就把该命令以追加的方式写入文件,然后redis重启时,会读取该文件,逐一执行命令的方式恢复数据;
redis追加写数据并不是直接写入硬盘,而是先拷贝到了内核缓冲区page cache,等待内核将数据写入硬盘。redis提供了三种写回硬盘的策略:
将某一时刻的内存数据,以二进制的方式写入磁盘。
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-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命令
? 匹配一个字符
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 … # 同时设置多个键值
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 # 获取字段数量
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 # 删除指定索引范围之外的所有元素
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 # 从集合中随机弹出一个元素
zadd key score1 member1 score2 member2 … # 向有序集合中加入一个元素和该元素的分数,如果该元素已经存在,则会用新的分数替换原有的分数。返回新加入到集合中的元素个数
缓存对象、常规计数、分布式锁、共享session信息等
聚合计算(并集、交集、差集)比如点赞、共同关注、抽奖活动等
排序场景,比如排行榜、电话和姓名排序等
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,这是为了避免客户端发生异常而无法释放锁。
优点:
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
原子性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的时候,会出现自己等待自己锁释放的情况,就会导致死锁。
一种内存可见性模型,解决因为指令重排序的存在,导致的数据可见性问题。对于两个操作A和B,这两个操作可以在不同线程中执行。如果A happens-before B,那么可以保证,当A操作执行完后,A操作的执行结果对B操作是可见的。
happens-before只是描述结果的可见,并不表示指令执行的先后顺序,也就是说只要不对结果产生影响,仍然允许指令的一个重排序。
Java 内存模型底层是通过内存屏障(memory barrier)来禁止重排序的。
使用一个voliate修饰的int类型的同步状态,通过一个FIFO队列完成资源获取的排队工作,把每个参与资源竞争的线程封装成一个Node节点来实现锁的分配。
提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。
线程首先尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个node节点加入到FIFO队列中。接着会不断的循环尝试获取锁,条件是当前节点为head的直接后继才会尝试。如果失败就会阻塞自己直到自己被唤醒。而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。
AQS使用一个int成员变量来表示同步状态,使用Node实现FIFO队列,可以用于构建锁或者其他同步装置
AQS资源共享方式:独占Exclusive(排它锁模式)和共享Share(共享锁模式)
AQS的功能分为两种:独占和共享
独占锁,每次只能有一个线程持有锁。
共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock
获取锁的步骤
同步工具,通过一个计数器来实现,初始值为线程的数量。每当一个线程完成了自己的任务,计数器的值就相应得减1。当计数器到达0时,表示所有得线程都已执行完毕,然后再等待的线程就可以恢复执行任务。
1.7 中 segment数组,segment继承ReentrantLock,也就是说每个segment对象就是一把锁,一个segment对象内部存在一个HashEntry数组,也就是说HashEntry数组中数据同步依赖同一把锁。不同HashEntry数组的读写互不干扰,这就是所谓的分段锁。
1.8
Java字节码运行在JVM中,而JVM运行在各个操作系统上,所以当JVM想要进行线程创建和回收的这种操作时,是必须要调用操作系统的相关接口,也就是说JVM线程与操作系统线程之间存在着某种映射关系。这两种不同维度的线程之间的规范和协议呢,就是线程模型。
JVM线程对不同操作系统的原生线程进行了高级抽象,可以使开发者一般情况下可以不用关注下层的细节,而只要专注上层的开发就行了。
在Linux系统中,Linux线程KLT(Kernel Level Thread)又被称为轻量级进程LWP(Light Weight Process)。
线程是抽象概念,因为Linux内核没有专门为线程定义数据结构和调度算法,所以Linux去实现线程的方式是轻量级进程,其实本质还是进程,只不过加了一个轻量级的修饰词。那么轻量级进程与进程之间的区别在哪呢?一个Linux进程拥有自己独立的地址空间,而一个轻量级进程没有自己独立的地址空间,只能共享同一个轻量级进程组下的地址空间。
当前Java虚拟机使用的线程模型是基于操作系统提供的原生线程模型来实现,Windows系统和Linux系统都是使用的内核线程模型,而Solaris系统支持混合线程模型和内核线程模型两种实现。
java内存模型规定所有成员变量都需要存储在主内存中,线程会在其工作内存中保存需要使用的成员变量的拷贝,线程对成员变量的操作(读取和赋值)都是对其工作内存中的拷贝进行操作。各个线程之间不能访问工作内存,线程变量的传递需要通过主内存来完成。
Java内存模型定义了8种原子操作来实现上图中的线程内存交互:
read,将主内存中的一个变量的值读取出来
load,将read操作读取的变量值存储到工作内存的副本中
use,把工作内存中的变量的值 传递给执行引擎
assign,把从执行引擎中接收的值赋值给工作内存中的变量
store,把工作内存中一个变量的值传递到主内存
write,将store操作传递的值写入到主内存的变量中
lock,将主内存中的一个变量标识为某个线程独占的锁定状态
unlock,将主内存中线程独占的一个变量从锁定状态中释放
对象实例化的模式,创建型模式用于解耦对象的实例化过程。
某个类只能有一个实例,提供一个全局的访问点。
双重检查锁,线程安全的单例模式代码实现:
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();
}
}
将对象组合成树形结构以表示“部分-整体”的层次结构。
对外提供一个统一的方法,来访问子系统中的一群接口。
通过共享技术来有效的支持大量细粒度的对象。
类和对象如何交互,及划分责任和算法。
定义一系列算法,把他们封装起来,并且使它们可以相互替换。
定义一个算法结构,而将一些步骤延迟到子类实现。
将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
四个角色:
java集合框架中,List和Set都继承自Collection接口,该接口声明如下
public interface Collection<E> extends Iterable<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
...
}
提供了一个iterator()方法,用于返回一个Iterator类型的迭代器对象,用来遍历聚合中的元素。
对象间的一对多的依赖关系。
用一个中介对象来封装一系列的对象交互。
在不破坏封装的前提下,保持对象的内部状态。
给定一个语言,定义它的文法的一种表示,并定义一个解释器。
允许一个对象在其对象内部状态改变时改变它的行为。
将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
不改变数据结构的前提下,增加作用于一组对象元素的新功能。
解耦
异步
流量削峰
数据分发
同时带的问题;
系统可用性降低、复杂性提高、一致性问题
提供了路由管理、服务注册、服务发现的功能,是一个无状态节点。nameserver是服务发现者,集群中各个角色都需要定时向nameserver上报自己的状态,以便互相发现彼此,超时不上报的话会被从列表中删除。nameserver可以部署多个,当多个namesever存在的时候,其他角色同时向他们上报信息,以保证高可用。nameserver集群间互不通信,没有主备的概念。nameserver内存式存储,nameserver中的broker、topic等信息默认不会持久化。
面向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
消息生产者。通过集群中的其中一个节点建立长连接,获得topic的路由信息,包括topic下面有哪些queue,这些queue分布在哪些broker上等。接下来向提供topic服务的master建立长连接,且定时向master发送心跳。
消息消费者。通过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(异步)
主要为消息生产者和消费者提供主题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
是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率。
零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上下文切换而带来的不必要开销。
传统的数据传输机制
buffer = File.read();
Socket.send(buffer);
比如读取文件,再用socket发送出去,实际经过了4次拷贝。
MMAP内存映射
硬盘上文件位置和应用程序缓冲区进行映射,由于mmap将文件直接映射到了用户空间,所以实际文件读取时根据这个映射关系,直接将文件从磁盘拷贝到用户空间,只进行了一次数据拷贝,不再有文件内从从硬盘拷贝到内核空间的缓冲区。
mmap内存映射:3次拷贝(1次cpu拷贝,2次DMA拷贝)
RocketMQ主要由Producer、Broker、Consumer三部分组成。Producer生产消息,Consumer消费消息,Broker存储消息。
两阶段提交,半事务,执行本地事务,事务回查
确保幂等性,防止消息重复消费
消费失败重试,即使成为死信也需要特殊处理
消息生产的默认选择队列策略:规避策略
消息生产的故障延迟机制策略:轮询+规避
先处理本地事务,再提交offset
controller层使用@Transactional注解是无效的。
默认spring事务只在发生未被捕获的 RuntimeException 时才回滚。
spring aop 异常捕获原理:被拦截的方法需显式抛出异常,并不能经任何处理,这样aop代理才能捕获到方法的异常,才能进行回滚,默认情况下aop只捕获 RuntimeException 的异常,但可以通过配置来捕获特定的异常并回滚
换句话说在service的方法中不使用try catch 或者在catch中最后加上throw new runtimeexcetpion(),这样程序异常时才能被aop捕获进而回滚
可以在controller层方法的catch语句中增加:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();语句,手动回滚。
@RequestParam注解(localhost:8080/demo?name=zhu) 如果参数过多,可以直接使用一个POJO对象来进行绑定,且不需要加@RequestParam注解
主要用于对前端请求参数进行约束,包括参数名不匹配问题、是否必须、默认值。
public String med(@RequestParam(name = “name”, required = false, defaultValue = “zhu”) String aa, Model model) {…}
required 表示是否必须,默认为 true,必须。
defaultValue 可设置请求参数的默认值。
value 为接收url的参数名(相当于key值)。
application/json时候,json字符串部分不可用,url中的?后面添加参数即可用,form-data、x-www-form-urlencoded时候可用
@PathVariable和@PathParam用于接收URL中占位符的参数(localhost:8080/demo/{name}/{id})
public void med(@PathVariable String name, @PathVariable int id) {…}
@RequestBody Map map / @RequestBody Object object
Content-Type为 application/json时候可用,form-data、x-www-form-urlencoded时候不可用
传播性(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时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。
首先用户发送请求至前端控制器DispatcherServlet
DispatcherServlet调用处理器映射器HandlerMapping,生成处理器对象返回
DispatcherServlet调用处理器适配器HandlerAdapter,经过适配调用具体的处理器Controller返回ModelAndView
DispatcherServlet将ModelAndView传给视图解析器ViewReslover,返回具体的View
DispatcherServlet根据View渲染视图响应用户
实际上就是从spring.facroties文件中获取到对应需要进行自动装配的类,并生成相应的bean对象,然后将它们交给spring容器管理。
@SpringBootApplication=>
@EnableAutoConfiguration=>
@Import({AutoConfigurationImportSelector.class})实现自动装配。
核心方法selectImport,读取META-INF/spring.factories文件,经过去重、过滤返回需要装配的类集合。
fatal > error > warn > info > debug > trace
默认打印info及以上级别日志。
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容器当中进行依赖注入。
场景: UserServiceImpl中注入UserMapper
@Autowired
private UserMapper userMapper;
在容器启动,为对象赋值的时候,遇到@Autowired注解,会用后置处理器机制,来创建属性的实例,然后再利用反射机制,将实例化好的属性,赋值给对象。
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
lombok注解也是使用构造器注入@RequiredArgsConstructor
private final UserMapper userMapper;
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper
}
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,它的实现与设计模式中的工厂模式和修饰器模式类似。
@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) {
...
}
}
@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;
}
@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));
}
}
@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)。
授权(RBAC实现授权)
RBAC 基于角色的访问控制(Role-Based Access Control)是按角色进行授权,当需要修改角色的权限的时候就需要修改授权的相关代码,系统可扩展性差。
if(主体.hasRole(“总经理角色id”)|| 主体.hasRole(“部门经理角色id”)){
查询工资
}
RBAC 基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,系统设计时定义好查询工资的权限标识,机试查询工资所需的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强。
if(主体.hasPermission(“查询工资的权限标识”)){
查询工资
}
Cross-origin resource sharing 跨域资源共享。
同源:协议、域名、端口号都相同。浏览器同源策略,是浏览器最核心也最基本的安全功能。
cors允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
浏览器端:
目前,所有浏览器都支持该功能(IE10以下不行)。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。
服务端:
CORS通信与AJAX没有任何差别,因此不需要改变以前的业务逻辑。浏览器会在请求中携带一些头信息,以此判断是否运行其跨域,然后在响应头中加入一些信息即可。
可以通过重写corsFilter或重写WebMvcConfigurer
Cross Site Request Forgery 跨站点请求伪造。
CSRF攻击是源于Web的隐式身份验证机制!Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。CSRF攻击的一般是由服务端解决。
spring security 默认开启csrf,过滤器CsrfFilter来判断是不是
OAuth 的核心就是向第三方应用颁发令牌。
OAuth 2.0 规定了四种获得令牌的流程,向第三方应用颁发令牌。
授权码(authorization-code)
隐藏式(implicit)
密码式(password):
客户端凭证(client credentials)
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
授权服务器:客户端注册、客户端授权、为获得授权的客户端颁发令牌、颁发刷新令牌并响应令牌刷新请求
1.创建oauth2认证服务
@EnableAuthorizationServer注解标注认证服务
@EnableWebSecurity注解标注Security认证配置
2.创建oauth2资源服务
@EnableResourceServer注解标注资源服务
@Insert(“insert into attachment (name) values (#{name})”)
@Options(useGeneratedKeys = true, keyProperty = “id”, keyColumn = “id”)
int insert(Attach attach);
批量返回自增主键
INSERT INTO user(name,pwd) VALUES
(#{u.name})
int i = attachMapper.insert(attach);
新增条数:i, 返回自增主键:attach.getId();
#和$都是实现动态sql的方式
#号占位符等同于JDBC里面一个?号占位符,它相当于向PreparedStatement里面预处理语句设置参数,而PreparedStatement里面的sql是预编译的,
$占位符相当于在传递参数的时候,直接把参数拼接到了原始sql里面,mybatis不会对它进行特殊处理。
一致性(Consistency):所有节点在同一时间具有相同的数据;
可用性(Availability) :保证每个请求不管成功或者失败都有响应;
分隔容忍(Partition tolerance) :系统中任意信息的丢失或失败不会影响系统的继续运作。
分布式是一个概念,是为了解决单个物理服务器容量和性能瓶颈问题而采用的优化手段。该领域涉及的问题比较多,如分布式锁、分布式缓存、分布式事务、分布式文件系统、分布式数据库等。
从理念上来说,分布式的实现方式有两种:
水平扩展:当一台机器扛不住流量时,就通过添加机器的方式,将流量平分到所有服务器上,所有机器都可以提供相当的服务;
垂直拆分:单一应用根据业务功能对系统进行拆分。
分布式系统中实现事务,它是由多个本地事务组合而成。
准备阶段和提交阶段。是一种强一致性设计,引入了一个事务协调者角色来协调管理各参与者的提交和回滚。
这个方案很少使用,一般来说某个系统内部如果出现这种跨多个库的操作,是不合规的。现在的微服务,一般要求每个服务只能操作自己对应的数据库。
相比于2PC,它在参与者中引入了超时机制,并且新增了一个预提交阶段,使得参与者可以统一各自状态。
2PC和3PC都是数据库层面的,而TCC是业务层面的分布式事务。
Try指的是预留,即资源的预留和锁定;Confirm指的是确认操作,这一步其实就是真正的执行;Cancel指的是撤销操作。
TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。还有一点要注意,撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等。
相对于 2PC、3PC ,TCC适用的范围更大,但是开发量也更大,毕竟都在业务上实现,而且有时候你会发现这三个方法还真不好写。不过也因为是在业务上实现的,所以TCC可以跨数据库、跨不同的业务系统来实现事务。
将业务的执行和将消息存入消息表的操作放在同一事物中,这样保证消息放入本地表中业务肯定是执行成功的。
大概流程:
最终一致性,比如RocketMQ支持消息事务。
第一步先给Broker发送事务消息即半消息,再根据本地事务结果向Broker发送提交或者回滚命令。
阿里seata
Seata的设计目标是对业务无侵入,因此从业务无侵入的2PC方案着手,在传统2PC的基础上演进。它把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务。
分布式锁是一种跨进程、跨机器节点的一种互斥锁。它用来保证在多个机器节点对共享资源访问的排他性。
线程锁的声明周期是单进程多线程,分布式锁是多进程多机器节点。
需要满足锁的排他性、可重入性,需要有锁的获取和释放方法以及锁的失效机制(避免死锁)。
实现方案
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分钟
瞬时高并发
页面静态化,以及秒杀按钮(通过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
扣减库存
// 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;");
产生原因
1、代码中存在死循环
2、定时任务跑批量
3、tomcat高并发项目的时候,所有线程都处在运行状态,消耗CPU资源
4、分布式锁的重试机制
a、乐观锁:能够保证用户线程一直在用户态,缺点是消耗CPU的资源
b、CAS自旋锁
解决
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 动态显示当前耗费资源最多的进程信息
tree
system
ifconfig 查看网络情况
ping 测试网络联通
telnet ip port : 查看某一个机器上的某一个端口是否可以访问,如:telnet 197.0.35.1 8080
netstat 显示网络状态信息
kill 杀死进程