面试八股文
Java基础
类生命周期
java源文件 –javac编译–>
java字节码 –类加载–>
class对象 –实例化–>
实例对象 —-> 卸载
python3
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialication)、使用(Using)和卸载(Unloading)七个阶段。
其中验证、准备和解析三个部分统称为连接(Linking)。加载、验证、准备、解析和初始化是类的加载过程。
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言地运行时绑定(也称动为态绑定)。
try-catch
加载:读取class文件,将字节流所代表的静态存储结构传化为运行时数据结构存储在方法区内,并在堆中生成该类class类型的对象的过程。
验证:文件格式(是否符合class文件格式规范,并且能够被当前版本虚拟机处理)、元数据(主要对字节码描述的信息进行语义分析)、字节码、符号引用(发生在虚拟 机将符号引用转化为直接引用的时候)
准备:主要为类变量(static)分配内存并设置初始值(数据类型默认值)。这些内存都在方法区分配。
解析:主要是虚拟机将常量池中的符号引用转化为直接引用的过程。
初始化:类构造器()方法执行
xgboost
类加载
类的加载是将.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆中创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
串口通信
启动类加载器Bootstrap Classloader 负责虚拟机启动时加载jdk核心类库以及后两个类加载器
扩展类加载器Extension Classloader 继承自ClassLoader,负责加载{JAVA_HOME}/jre/lib/ext/目录下所有的jar包
应用程序类加载器Application Classloader 是Extension ClassLoader的子对象,负责加载应用程序classpath目录下所有的jar和class文件
系统相册
双亲委派机制
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,子加载器才会尝试执行加载任务。
双亲委派可以避免重复加载,父类已经加载了,子类就不需要再次加载; 更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。
AA系统
jdk、jre、jvm
JDK是Java开发工具包,包括jre和一些工具javac(编译)、java、javap(反编译)、jconsole(java虚拟机执行状况监视器,用来监控虚拟机内存、线程、cpu使用情况以及相关得java进程MBean)等
JRE是Java运行时环境
JVM是Java Virtual Machine
app
JVM
Java Virtual Machine Java虚拟机
Java是一门抽象程度很高的语言,提供了自动内存管理特性。
java具有跨平台语言,一次编译,到处运行。
Java虚拟机主要包括运行时数据区、类加载子系统和字节码执行引擎等。
类加载子系统:负责加载程序中类和接口;
执行引擎:执行字节码文件和本地方法;
Java虚拟机的运行时数据区在内存中,所以这部分也称为JVM内存模型
列表
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,不受堆内存大小限制,此时,只会受限于本地内存大小。
INSTALL_FAILED
最后需要注意的是: 调整方法区(元空间)的大小会发生Full GC,这种操作的代价是非常昂贵的。如果发现应用在启动的时候发生了Full GC,则很有可能是方法区(元空间)的大小被动态调整了。
匹配本地存储的组员名
所以,为了尽量不让JVM动态调整方法区(元空间)的大小造成频繁的Full GC,一般将-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置成一样的值。例如,物理内存8G,可以将这两个值设置为256M
排序算法
JVM内存模型
-
虚拟机栈:描述了Java方法执行时的内存模型,即每个方法执行的时候,线程都会在自己的线程栈中同步创建一个栈帧,用于存放局部变量表、操作数栈、动态连接和方法出口等信息。
局部变量表:保存方法参数和局部变量;
操作数栈:
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
每个方法从调用到完成的过程,就对应着一个栈帧在线程栈中从入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError栈溢出异常(单线程独有)
如果虚拟机在动态扩展栈时无法申请到足够的内存空间,抛出OutOfMemoryError异常(多线程会发生)
在Java中,所有的基本数据类型(byte、short、int、long、float、double、boolean、char)和引用变量(对象引用)都是在栈中的。一般情况下,线程退出或者方法退出时,栈中的数据会被自动清除。docker搭建gitlab
-
本地方法栈:与虚拟机栈作用类似,不同的是虚拟机栈为JVM执行的java方法服务,而本地方法栈为JVM调用的本地方法服务。
HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一。
程序计数器:每个线程都会有自己独立的程序计数器,主要功能是记录当前线程执行到哪一行指令了。刮刮乐
-
方法区:保存类型信息(类签名、属性、方法)
存放虚拟机加载的类的元信息、常量池、静态变量等的引用,以及即时编译器编译后的代码缓存等数据。
jdk8后抛弃了永久代的概念,通过在本地内存中实现了元空间代替永久代,并且将常量池和静态变量移到Java堆区。所以方法区是使用直接内存来实现的,这与堆是不一样的,也就是堆和方法区不在同一块物理内存。直接内存并不是JVM运行时数据区的一部分,其分配不会受Java堆大小的限制。cmd
-
堆:存放对象实例,是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
ieee论文
CMS:
1.初始标记:stw
2.并发标记:三色标记算法
3.重新标记:stw
4.并发清理:
单链表
三色标记:
并发标记阶段。
黑色:标记完,孩子标记完;
灰色:自己已经标记完,还没来得及标记fields;
白色:没有标记到的节点;
图像分类
集合
-ArrayList中维护一个Object类型数组。当使用无参构造创建时,默认容量为10,扩容时按照当前容量的1.5倍扩容,即 10 –> 15 –> 22 …
Vector 也是List接口一个实现类,是线程安全的,扩容时按2倍扩容。
LinkedList 双向链表,添加和删除效率高。
typora
HashSet底层是HashMap实现的。添加元素时,先通过元素哈希值得到table数组索引。如果该索引位置没有其他元素,直接添加;如果该位置已经有元素,则需要进行equals方法判断,相等则不添加,不等则以链表方式添加。
扩容:第一次添加默认16,临界值为16*0.75=12,0.75为默认加载因子。
如果数组长度到了12,就扩容16 * 2 = 32, 新的临界值为32 * 0.75 = 24。
jdk8中,如果一条链表元素个数到达8,并且table的大小大于等于64,就会转红黑树。否则仍然采用数组扩容机制。
WSA
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其实是伪装成流的字节数组(把它们当成字节数据来看就好了),他们不会锁定任何文件句柄和端口,如果不再被使用,字节数组会被垃圾回收掉,所以不需要关闭。
在循环中创建流,需要在循环中关闭流。因为在循环外关闭,关闭的是最后一个流。
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视图
缓存
缓存问题
缓存雪崩:大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
缓存击穿:缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
缓存穿透:数据既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
缓存更新策略
常见的缓存更新策略共有3种:
- Cache Aside(旁路缓存)策略;
应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。
写策略;先更新数据库,再删除缓存;
读策略:如果命中缓存直接返回,如果没有命中,从数据库读取数据,然后将数据写入到缓存。 - Read/Write Through(读穿 / 写穿)策略;
- Write Back(写回)策略;
实际开发中,Redis 和 MySQL 的更新策略用的是 Cache Aside,另外两种策略应用不了。
更新缓存:
1.先写缓存,再写数据库
2.先写数据库,再写缓存
3.先删缓存,再写数据库
4.先写数据库,再删缓存
第一种方案先写缓存情况下,如果网络异常,第二步写数据库失败,这样缓存中的数据就变成了脏数据。这种法案实际工作中用得不多。
第二种方案先写数据库,再写缓存,如果并发量较高,为了防止出现大事务造成的死锁问题,通常建议写数据库和缓存不要放在同样一个事务。也就是说不加事务,如果写数据库成功了,但写缓存失败了,数据库中已写入的数据不会回滚。
在高并发场景,如果多个线程同时执行先写数据库操作,再写缓存,可能会出现数据库是新值,而缓存中是旧值,两边数据不一致。
也就是说该方案在高并发场景不适合。
第三种方案先删缓存,再写数据库。在高并发下,也会出现缓存和数据库不一样的情况,可以“缓存双删”,即在写数据库前删一次,写完后再删一次,第二次删并非立马删,而是间隔一段时间后再删。
第四种方案先写数据库,再删缓存出现问题的概率很小,相对其他方案来说是最小的。
先写数据库再删缓存的方案,跟缓存双删的方案一样,有一个共同的风险点,即如果缓存删除失败,也会导致缓存和数据库数据不一样。可以通过加重试机制,可以在更新缓存失败的情况下,重试三次。在接口直接同步重试,如果在该接口并发比较高的时候,可能有点影响接口性能。可以改成异步重试。
异步重试可以通过把重试数据写表,通过定时任务(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日志:每执行一条写操作,就把该命令以追加的方式写入文件,然后redis重启时,会读取该文件,逐一执行命令的方式恢复数据;
- RDB快照:将某一时刻的内存数据,以二进制的方式写入磁盘;RDB快照恢复数据效率会比AOF高,但redis快照是全量快照,也就是每次执行,都把内存中所有数据都记录到磁盘中,所以快照是一个比较重的操作。
- 混合持久方式:集成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再定时同步到数据库 - 限流
int类型,incr方法
以访问者的ip和其他信息作为key,访问一次增加一次计数,超过次数则返回false - 位统计
string类型的bitcount方法 - 购物车
string或hash
key:用户id;field:商品id;value:商品数目
List类型
消息队列
9. 用户消息时间线timeline
list,双向链表,直接作为timeline,插入有序
10. 消息队列
list提供两个阻塞操作blpop/brpop,可以设置超时时间
11. 抽奖
自带一个随机获得值
spop myset
Set类型
聚合计算(并集、交集、差集)比如点赞、共同关注、抽奖活动等
12. 点赞、签到、打卡
sadd like:abc zhu // 点赞
srem like:abc zhu // 取消点赞
sismember like:abc zhu // 是否点赞
smembers like:abc // 点赞所有用户
scard like:abc // 点赞数
13. 商品标签
sadd tags:abc abc
14. 商品筛选
sdiff set1 set2 // 获取差集
sinter set1 set2 // 获取交集
sunion set1 set2 // 获取并集
15. 用户关注、推荐模型
sadd 1:follow 2
sadd 2:fans 1
sinter 1:follow 2:fans
Zset类型
排序场景,比如排行榜、电话和姓名排序等
16. 排行榜
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 是跨集群部署的,自然就避免了单点故障)。
并发编程
并发编程三个问题
- 原子性
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节点,加入到队列中并挂起当前线程
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) - 迪米特法则
迪米特法则又叫做最少知识原则,就是说一个对象应当对其它对象又尽可能少的了解,不和陌生人说话。 - 合成复用原则
合成复用原则要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
创建型模式
对象实例化的模式,创建型模式用于解耦对象的实例化过程。
- 单例模式
某个类只能有一个实例,提供一个全局的访问点。 - 工厂模式
一个工厂类根据传入的参量决定创建出哪一种产品类的实例。 - 抽象工厂模式
创建相关或依赖对象的家族,而无需明确指定具体类。 - 建造者模式
封装一个复杂对象的创建过程,并可以按步骤构造。 - 原型模式
通过复制现有的实例来创建新的实例,java对象通过实现Cloneable接口来实现复制。
浅拷贝
对于数据类型是基本数据类型及string类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。“string”属于Java中的字符串类型,也是一个引用类型,并不属于基本的数据类型。
对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
浅拷贝是使用默认的clone()方法来实现
深拷贝
复制对象的所有基本数据类型的成员变量值
为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝
实现方式1:重写clone方法来实现深拷贝
实现方式2:通过对象序列化实现深拷贝(推荐)
结构型模式
把类或对象结合在一起形成一个更大的结构。
- 装饰器模式
动态的给对象添加新的功能。 - 代理模式
为其它对象提供一个代理以便控制这个对象的访问。 - 桥接模式
将抽象部分和它的实现部分分离,使它们都可以独立的变化。 - 适配器模式
将一个类的方法接口转换成客户希望的另一个接口。 - 组合模式
将对象组合成树形结构以表示“部分-整体”的层次结构。 - 外观模式
对外提供一个统一的方法,来访问子系统中的一群接口。 - 享元模式
通过共享技术来有效的支持大量细粒度的对象。
行为型模式
类和对象如何交互,及划分责任和算法。
- 策略模式
定义一系列算法,把他们封装起来,并且使它们可以相互替换。 - 模板模式
定义一个算法结构,而将一些步骤延迟到子类实现。 - 命令模式
将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。 - 迭代器模式
一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。 - 观察者模式
对象间的一对多的依赖关系。 - 仲裁者模式
用一个中介对象来封装一系列的对象交互。 - 备忘录模式
在不破坏封装的前提下,保持对象的内部状态。 - 解释器模式
给定一个语言,定义它的文法的一种表示,并定义一个解释器。 - 状态模式
允许一个对象在其对象内部状态改变时改变它的行为。 - 责任链模式
将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。 - 访问者模式
不改变数据结构的前提下,增加作用于一组对象元素的新功能。
消息队列
rocketmq
使用场景
解耦
异步
流量削峰
数据分发
同时带的问题;
系统可用性降低、复杂性提高、一致性问题
rocketmq角色
NameServer
Broker:消息存储
Producer
Consumer
基本概念
主题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分布式事务
两阶段提交,半事务,执行本地事务,事务回查
确保幂等性,防止消息重复消费
消费失败重试,即使成为死信也需要特殊处理
消息生产的默认选择队列策略:规避策略
消息生产的故障延迟机制策略:轮询+规避
Spring&Springboot
注解
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时候不可用
- 在GET请求中,不能使用@RequestBody
- 在POST请求,可以使用@RequestBody和@RequestParam,但是如果使用@RequestBody,对于参数转化的配置必须统一。
- 可以使用多个@RequestParam获取数据,@RequestBody不可以
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时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。
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) :系统中任意信息的丢失或失败不会影响系统的继续运作。
服务器
常见负载均衡算法
- 随机
- 轮询
- 源地址哈希法
通过对发送请求客户端ip地址进行求hash值,并对服务器地址列表长度取余,选择结果对应的服务器。该方法保证同一个客户端ip地址会被映射到相同的后端服务器,可以保证服务消费者和服务提供者之间建立有状态的session会话。 - 加权轮询法
给每个服务器都设置权重,配置低、负载高的服务器权重低,配置高、负载低的服务器权重高。让权重高的服务器接收到请求的概率更高。 - 最小连接算法
前面几个算法基本只考虑了请求数上的负载均衡,而没有考虑到每个请求处理时长。最小连接算法根据每个服务器当前连接的请求来选择连接请求数最小的服务器,因此该算法需要为每个服务器地址维护一个连接数变量来记录当前服务器连接的请求数。 - 加权随机法
与加权轮询法类似,加权随机法也是根据后端服务器不同的配置和负载情况来配置不同的权重。不同的是,它按照权重来随机选择服务器,而不是顺序。