spring 的ioc
讲ioc之前,我觉得先要了解依赖倒置原则就是高层模块不能直接依赖于底层模块,应该依赖于底层模块的抽象,这样做是为了解开代码的耦合,然后我们开发的时候有了这个设计模式,我们还是得解决创建对象得问题,如果需要调用方去创建这些对象的话,是很难维护的,此时我们就需要一个帮忙创建对象并且管理对象的容器,ioc则就是充当这一角色,而di是ioc的一种实现,ioc的意识是控制反转是指本该在由程序代码里自己创建对象的,但是把这个控制权交给了ioc容器,由容器帮我们创建这些对象,然后根据依赖关系把这些对象注入到对于的高层模块。
注意回看设计模式的6大原则
spring aop
切面:就是切点和通知的结合
通知:分为前置增强,后置增强,环绕增强
连接点:是指能过插入切面的点,就指的是方法
切点:就是能过插入增加的连接点
织入:就是把增强添加到目标对象的过程
目标对象:就是增强的类
代理对象:就是织入增强之后生成的代理对象
主要的应用就是日志,事务
生成代理对象使用的是jdk的动态代理,在目标对象没有接口时使用cglib生成动态代理对象
动态代理的逻辑
就是根据目标类生成一个动态代理对象,动态代理类中调用的方法其实就是invocationHandle中的invoke方法,这就是我们为什么还是需要依赖一个目标对象,因为在invoke方法中我们需要调用目标方法的具体实现。
aop的简单原理
spring bean 的生命周期
实例化
属性注入
初始化
销毁
在实例化之后可以看bean有没有实现BeanNameAware,BeanFactoryAware,ApplicationContextAware的接口,如果有先执行对于的方法,如果自己实现了BeanPostProcessor(bean后处理) 的接口,里面有两个方法,一个是在初始化前,一个是在初始化后执行的,用于bean的加强,不过这个是容器级别的处理,会让所有bean在初始化的的时候都经过这个处理
spring 事务的传播机制和隔离级别
传播机制:有七种
1.required(必须的):如果当前没有事务,则新建一个事务,如果当前有事务则加入当前事务
2.supports(支持):如果存在事务则加入事务,如果没有事务则以非事务的形式进行
3.mandatory:(强制的):如果存在事务则加入事务,如果没有则抛错
4.required_new(必须开新的),如果当前有事务,挂起当前事务,然后开启另一个事务执行方法,就例如当前事务为A,A会被挂起,然后开启B事务,如果B事务回滚,那么AB都回滚,如果A回滚,那么B不会回滚而是正常提交
5.not_supported(不支持)如果当前有事务,则挂起事务,然后始终不以事务执行
6.never(从不):从不以事务执行,如果当前事务存在则抛出
7.nested (嵌套):外层回滚,内层一样回滚
事务隔离级别
1.读未提交
2.读已提交
3.可重复的
4.串行化
springboot 的启动流程
springBoot中@SpringBootApplication中的@SpringBootApplication会@Import一个Bean"AutoConfigurtionImportSelector",通过getCandidateConfigurations扫描 META-INF/spring.factories的文件,文件内是是一组组key-value保存 需要autoConfig的类,springboot会根据这些类上的@ConditionXXX的注解决定是否把该类加载入容器中@EnableConfigurationProperties则是加载配置属性,这些属性也可以自己在properties,yml文件中自己定义,这就是约定大于配置的核心
热部署原理,就是有两个classLoader,一个负责加载不会改变的第三方jar,一个负责加载会更改的类。
springcloud 的相关面试题
什么是微服务:微服务是分布式的一种架构,把单一的项目体系拆分成多个服务,服务间采取轻量级通讯机制进行沟通,且每个服务可单独构建部署(soa?)
nacos 和 eureka 和zoomkeep 的区别
两者都是用ap,但是nacos也支持cp, zoomkeep是cp的
eureka注册中心集群是去中心化的,nacos是会选取一个领导者,zoomkeep也是会选取一个领导者
集群中数据同步:
nacos 以发生心跳的方式发送给跟随者,过半的跟随者更新成功该数据,则通知其他节点也更新该数据
eureka是让注册信息发送给个个节点
zoomkeep是领导者写入数据之后,然后通过2pc(两段式提交)的方法去同步数据,在此期间zoomkeep是不可用的
nginx和ribbon
nginx是用户端发送请求到nginx,然后由nginx服务端实现负载均衡转发
ribbon是在客户端进行转发,是从注册中心获取服务列表,然后再负载均衡
ribbon的面试题
ribbon有六种负载均衡算法
轮询
随机
根据响应时间的快慢加权
并发量最小的
通过性能和可用性加权
过滤掉熔断的服务
seata (AT模式)
分为三个角色
TC(Transaction Coordinator)事务协调器,负责协调驱动全局事务的提交或者回滚
TM(Transaction Manager)事务管理者,负责发起一个全局事务,并且发起全局事务的提交或者回滚的决议
RM(Resource Manage)分支事务管理者,控制分支事务的本地提交,和注册分支到TC ,和接受TC全局事务提交或者回滚的指令。
主要流程是TM向TC申请一个新的全局事务并且生成一个全局事务id(xid)
然后每个RM向TC注册分支事务,RM会先执行本地SQL,和生成一个Undo记录,然后尝试获取全局锁(Fescar的全局写排它锁解)如果其他全局事务占用了这个锁,则进行重试,如果获取不到则回滚本地事务,删除undo记录,告诉TC分支事务执行失败。如果获取全局锁成功则提交本地事务,且保持锁的拥有。
如果TC驱动全局事务提交,那么分支事务先释放全局锁,然后开启异步删除undo记录,并且直接返回成功给TC,
如果TC驱动的全局事务回滚,那么RM则会开启一个本地事务,查询自己的undo记录,对比undo的after_image和当前记录是否一样,一样则按照before_image进行回滚,不一样则根据设置的策略进行回滚,如果没有设置则按照before_image进行回滚。
after_image和当前记录是怎么比较的?
XA模式(使用XA协议)
和AT模式一样,对业务无入侵的方式,不是补偿性的,是数据一致性很好的
流程:
TM向TC申请一个全局事务
RM分别向TC注册分支,然后执行业务SQL,xa-prepare,此时进入阻塞状态,等待TC的驱动指令
因为分支事务时没有提交的,这时TC驱动什么命令就按照命令执行回滚还是提交。
(MT模式)TCC
分为三个部分
try(尝试)
confirm(确定)
cancel(回滚)
最终一致性方案
Hystrix:
什么时服务雪崩:就是服务间调用,长时间等待,导致上游服务请求堆积,最终上游服务资源耗尽,并且一直往上蔓延导致雪崩。
服务降级,服务熔断
CAP
C:一致性 当某个节点接到修改数据的请求,会同步数据到其他节点,只有所有节点都获取最新数据系统才可用,如果有的节点由于网络之类的原因导致系统无法达到一致性,那么为了达到对外数据的一致性,未同步到数据的节点就只能返回空数据。
A:可用性 就是只要节点接受到了请求,都需要正常的返回数据,不管数据是否一致
P:分区容错性 可能是节点间出现网络通讯异常,或者有的节点宕机了,这时候就会出现分区,系统不会因为分区而导致整个系统不可用,这就是分区容忍性。
由于分布式系统中,为了提高分区的容忍性,就需要把节点复制到更多的节点,然后同步数据的难度就上升,需要等待所有节点数据都同步的时长变长,这是可用性就难以保证。节点越多分区容忍性越高,一致性越难保证,等待的时间越长,可用性就降低。
BASE理论:就是强一致性太难,我们可以达到最终一致性
hashmap currentHashmap
hashMap:1.7实现时数组+链表 1.8实现是数组+链表+红黑树
链表插入方式从头插法改成尾插法。
1.8的put方法流程
红黑树插入TreeNode:先找到root(根节点)把root 赋值给p 然后从根节点开始遍历,判断key的hash是节点的左边还是右边,如果是左边就把左子节点赋值给,如果是右边就把右子节点赋值给p
然后继续遍历,知道左子节点或者右子节点为空时说明找到了对应的位置,新建一个节点,建立节点之间的关系,链表,和红黑树的关系。
扩容流程
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧数组容量
int oldThr = threshold; // 旧数组阈值
int newCap, newThr = 0;
// 如果旧数组容量大于0
if (oldCap > 0) {
// 如果大于最大容量则直接返回旧数组
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 容量加一倍,阈值加一倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 说明调用的时空参构造方法,需要对容量和阈值进行初始化
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 重排红黑树节点
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 重排链表节点
Node<K,V> loHead = null, loTail = null; // 数组[old]的头和尾
Node<K,V> hiHead = null, hiTail = null; // 数组[old + oldcap] 的头和尾
Node<K,V> next;
do {
next = e.next;
// 说明该节点还是会在数组原来的位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 相应的把链表的头节点放到数组中去
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
主要流程
先判断oldcap大于0,则根据oldCap和旧的阈值进行翻倍,如果不大于0,则说明该hasmap需要初始化,把需要默认值传入进行初始化,链表部分,根据hash与运算oldCap,得到该节点是在那个原来的位置,还是在原来+oldcap的位置上,然后使用尾插法进行链表排序,最后把链表的头设置到新数组中去。
Hash方法 用key的低16位异或高16位(异或是指相同则为0,不同则为1)也叫扰动函数,为了避免hash碰撞
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
indexFor方法 使用hash值与数组长度减1进行与运算(都为1则为1否则为0)
static int indexFor(int h, int length) {
return h & (length-1);
}
1.7头插法会导致循环链表
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
//1, 获取旧表的下一个元素
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
就是在多线程的环境下。
数组位置0上是一个链表 5 ->9, 此时有线程A和B对put数据,导致A,B都要进行扩容的操作
假设当A线程在刚刚开始遍历到节点e=5的时候,执行到Entry<K,V> next = e.next;阻塞了,但是接下来B线程已经完成了扩容,把链表 5 ->9使用头插法改成了,链表 9 ->5,这时线程A恢复,继续执行代码e.next = newTable[i];这时改变5这个节点next的指针为 newTable[i],因为线程B已经把链表该成9 ->5,所以newTable[i] = 9 的,这个时候两个节点的next指针就会互相指向了,导致循环链表的出现,此时就会进入死循环。
concurrenthashmap
put:
1.如果需要初始化,则调用初始化方法,初始化时会根据sizeCtl判断当前初始化的状态,0则代表可能没有人在初始化,尝试自己线程修改sizeCtl为-1,成功则进行初始化,初始化成功则把sizeCtl设置为下一次扩容的大小值。
2.先通过hash &(n -1)得到i,如果tab[i] == null 通过cas尝试修改成put进来的节点
3.如果不为空,则加锁遍历链表或者红黑树,进行修改值或者添加节点
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key,value都不能为空
if (key == null || value == null) throw new NullPointerException();
获取hash
int hash = spread(key.hashCode());
int binCount = 0;
// 死循环只有break,和return才能出来
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 初始化,initTable里面会先判断一个sizeCtl的值是否为0,如果为0,
// 则cas尝试把0改为-1(代表正在初始化)修改成功的线程进行初始化然后把sizeCtl改成下次扩容的大小。
// 如果发现是已经是小于0,那么挂起线程(就是让出cpu时间,然后大家再一起抢)
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 该table节点上为空则尝试通过cas去把这个值改成put进来的节点,成功则break推出循环
// 否则继续进入死循环
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果发现f.hash == MOVED(-1) 那么说明正在进行扩容
// 此时加入帮助扩容(这点不是太明白0.0)
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 如果以上场景都不符合,则加锁,进行添加节点和修改节点操作和hashmap操作差不多
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 最后给当前容量+1,判断是否需要扩容
addCount(1L, binCount);
return null;
}
transfer 转移 扩容方法
// 这个方法没有看的很懂
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
// 扩容一倍
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
// 相当于扩容期间的一个临时容器
// 会在最后处理完成之后把替换掉tab tab = nextTab
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 就在这个条件下把tab替换成nextTab,说明整个扩容完成
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 还是通过尾插法,把数据排成链表
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
// 把tab[i]节点改成fwd(里面其实装的是一整个nextTable)
// 在put方法中发现节点是fwd的,那么就可以知道是在扩容中
// 在get方法中,发现节点是fwd,这里就会调用fwd.find方法查找节点数据
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
get
由于table时volatile修饰的,所以是具有可见性的,就是volatile修饰的变量的写是before于读操作之前的,所以其他线程的修改是可见的
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果为头节点则直接返回
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 这里如果是当前正在扩容,find方法是调用ForwardingNode.find
// 通过查找ForwardingNode中nextTable的数据返回结果
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//如果不在扩容则正常遍历取节点
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
ForwardingNode
/**
* A node inserted at head of bins during transfer operations.
*/
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
}
重新看mysql,myslq优化策略 先复习明天
mysql的建表规则
1.选择合适的数据类型,字符类型尽量使用varchar变长类型,因为char是定长的,char(10)存入少于10个字符,不足的会补充空格。
2.尽量字段都设置成不为空的,给字段设置默认值
3.有小数点的数据用decimal
mysql索引建立的位置
1.表和表之间的连接点需要建立索引
2.经常搜索的列
3.经常范围查询的列
mysql常见的sql优化,explain
explain的主要几个字段,type,extra
type的几个关键词
const:主键或者唯一的二级索引精确的匹配到某个值的时候
eq_ref:当被驱动表的使用到主键或者唯一二级索引进行列的等值匹配时
ref:当被驱动表使用不同二级索引进行等值匹配时
unique_subquery:当优化器选择把in的子查询优化成exists语句时使用到的是主键或者唯一二级索引时
index_subquery:和上面的一样,只是用到的时一个普通的二级索引
range:使用到索引进行范围查询时
index:二级索引扫描
all:全表扫描(聚族索引)
extra会出现的几个比较关键的词
using index: 就是查询的字段都在使用的索引上,这种情况也称之为使用了覆盖索引。
using where: 如果是全表扫描的话,有使用到where过滤数据的话会出现
using join buffer 如果在链表查询时,被驱动表没有在连接点建立索引可能会出现,因为对于被驱动表而言不能使用索引会导致效率很低,mysql5.6之后加了这个缓冲区的功能提高效率。
using filesort:如果order by字段上没有索引就会出现using filesort,如果需要排序的数据比较少会在内存中进行这个排序,如果数据比较多,会在磁盘上进行这个排序。
using temporary 如果在去重,union,groupby 的时候不能使用到索引的话,那么就会出现这个,就会借助临时表完成去重,排序操作。
什么情况下索引失效
mysql索引的原理
hash 索引
就是建立索引的列上的值进行hash,如果hash碰撞就用链表连起来,索引保存的是行的指针
hash索引只能值配对,不能做范围查询
b+树索引
使用b+树作为索引得构建
是一个多路查找树
非叶子节点不保存数据,只保存保存键值和一个指针
叶子节点保存数据,每个叶子节点连成链表
因为mysql每次从磁盘读取大小为16k,也就是mysql每一页为16k,一般我们的主键用的是bigint(8字节)+ 一个指针大概是6个字节,所以一页大概会有16*1024/(8+6) ≈ 1000,也就是说高度为3的树,可以存放 1000 * 1000 * 1000 的值
倒排索引
mysql事务以及mvcc
怎么处理group by查询其他字段的问题
group_concat(其他需要的字段)
any_value(其他需要的字段)
max()
min()
这几个都是可以处理的
jvm,gc的内容 jvm调优,oop面向对象理解 后天务必完成
频繁fullGc怎么优化
项目总结 后台基本完成
看看多线程的东西
设计模式
六大原则
单一职责原则
开闭原则
依赖倒置原则
里氏替换原则
接口隔离原则
迪米特原则
项目下单使用了简单工厂模式+模板方法,因为下单流程是比较固定的,1.校验传入进来的参数是否合法,2.获取商品信息,3.生成订单和订单项数据,4.调用营销服务进行价格计算,5替换掉订单和订单项的价格相关信息,6扣减库存,7核销优惠券,然后建立一个接口里面有这几个方法,然后让每种类型下单进行实现这个接口去实现具体方法,同时也会有一个common类去继承这个接口,把通用的全实现在一个common里面,在每种类型下单时也可以直接用common的方法,如果有需要则在自己的具体实现修改,然后新建一个简单工厂通过传入的type判断返回那种类型,如果面试官问有100种类型要怎么办,(这时候就去怼面试官,谁项目有这么多种下单,你行你来)
关于上面的问题解决可以参考 https://blog.csdn.net/weixin_44204411/article/details/108885382
就是把类型当成一个key,具体的实现bean当初一个value存在一个map里面,在每种类型下单的实现类上加上一个自定义注解,然后通过Map<String, Object> beanMap = springContextUtil.getContext().getBeansWithAnnotation(AlarmAnnotation.class); 类似这样的写法获取bean,AlarmAnnotation(自定义注解),然后遍历等到的map,把他依次的加入到我们准备好的map。
目前还不太清楚这个是模板方法还是策略模式
处理幂等性
rabbitmq
概念
broker:表示消息队列服务器实体
Exchange:交换机,是接受生产者发送的消息并且把消息路由到对应的队列中
Binding:绑定,用于队列和交换机之间的关联,一个绑定就是根据路由键将队列和交换机绑定起来的
所以说交换机是绑定组成的一个路由网络表
Queue:队列,用于存放消息的知道发送给消费者,是消息的存放容器,一个消息可以投放到多个队列中。
Virtual Host:是虚拟主机,包含了Exchange,Binding,Queue,是共享相同身份认证和加密环境的独立服务器,相当于一个mini的Rabbitmq。
Exchange类型
direct:单播模式,routingKey就是消息队列名称
fanout:分列模式,发送到与该交换机绑定的消息队列上,没有routingKey,只要绑定上关系的消息队列就能接到交换机发送过来的消息
topic:主题模式,让交换机和队列绑定时添加自定义的routingKey,例如:ruting_key_queue1,
然后交换机通rutingKey去找到对应的队列,*代表匹配一个任意字符,#代表匹配一个或者多个字符。
/**
* 主题模式队列
* <li>路由格式必须以 . 分隔,比如 user.email 或者 user.aaa.email</li>
* <li>通配符 * ,代表一个占位符,或者说一个单词,比如路由为 user.*,那么 user.email 可以匹配,但是 user.aaa.email 就匹配不了</li>
* <li>通配符 # ,代表一个或多个占位符,或者说一个或多个单词,比如路由为 user.#,那么 user.email 可以匹配,user.aaa.email 也可以匹配</li>
*/
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(RabbitConstant.TOPIC_MODE_QUEUE);
}
@Bean
public Queue queueTwo() {
return new Queue("queue2");
}
@Bean
public Binding topicBinding2(Queue queueTwo, TopicExchange topicExchange) {
return BindingBuilder.bind(queueTwo).to(topicExchange).with("ruting_key_queue1");
}
可以用这个注解帮忙生成交换机,队列,以及他们之间的绑定关系,同时这个注解作为消费者的监听
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "topic.n1", durable = "false", autoDelete = "true"),
exchange = @Exchange(value = "topic.e", type = ExchangeTypes.TOPIC),
key = "r"))
如何保证消息发送出去了,且成功给消费了
rabbitmq:
# 开启发送端抵达队列确认【发送端确认机制+本地事务表】
publisher-returns: true
# 开启发送确认【发送端确认机制+本地事务表】
publisher-confirm-type: correlated
# 只要抵达队列,优先回调return confirm
template:
mandatory: true
# 使用手动确认模式,关闭自动确认【消息丢失】
listener:
simple:
acknowledge-mode: manual
发送方开启消息发送到broker成功的消息回调
发送方开启消息没有抵达队列的回调
消费方使用手动ack
如果真的需要保证消息一定发送成功且成功消费,那么就建一张表,里面保存消息的内容,交换机名称,路由键,消息状态(新建,已到达broker,发送失败,消费成功),在发送消息之前新建一条记录,拿到他的id,放入到消息当中去,然后根据到达broker的成功回调则修改该记录状态已到达broker,在消息不能到达队列的回调中把状态改成发送失败。(这个也可以当作防止重复消费,就是如果时消费成功状态就直接ack)
如何保证消息不被重复消费
保证消息的幂等性
在发送消息之前,生成一个token,把这个token存放到redis中
每个消息里面存放一个token
在消费端,去redis中查找这个token,有则进行后续处理,然后最后把这个token删除
死信,死信队列,死信交换机
就是把一个消息发送到一个设置了消息过期时间的队列中,这个过期时就叫私信,这个队列就叫做死信队列,死信队列还需要设置一个死信交换机,和一个路由key,就是需要把这些死信最后交到一个队列中处理。
痛定思痛
看排序,看有没有时间看看算法,其他算法以后再学习了。
冒泡,选择,快速
简单的排序算法
public class MaoPaoSort {
public static void main(String[] args) {
int[] arr = {9,3,10,89,94,-1,-29};
maopao(arr);
System.out.println(Arrays.toString(arr));
}
private static void maopao(int[] arr) {
for (int i = 0; i < arr.length ; i++) {
for (int j = 0; j <arr.length - i -1 ; j++) {
if (arr[j] >arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}
public class SelectSort {
public static void main(String[] args) {
int[] arr = {9,3,10,89,94,-1,-29};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void selectSort(int[] arr) {
for (int i = 0; i <arr.length ; i++) {
int minIndex = i;
int min = arr[i];
for (int j = i+1 ; j < arr.length ; j++) {
if (min > arr[j]) {
min = arr[j];
minIndex = j;
}
}
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
}
public class QuickSort2 {
public static void main(String[] args) {
int[] arr = {9,3,10,89,3,94,-1,-29};
quicksort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
private static void quicksort(int[] arr, int leftIndex, int rightIndex) {
// 递归一定要找到终点返回
if (leftIndex >= rightIndex) {
return;
}
int left = leftIndex;
int right = rightIndex;
int key = arr[left];
while(left < right) {
while (left < right && arr[right] >= key) {
right --;
}
arr[left] = arr[right];
while (left < right && arr[left] <= key) {
left ++;
}
arr[right] = arr[left];
}
arr[left] = key;
quicksort(arr, leftIndex, left -1);
quicksort(arr, right + 1, rightIndex);
}
}
多线程场景
每天定时重新推送已支付但是推动oms状态为未支付的订单,先查出有多少这样的订单,然后就按照分页的形式开启多个线程去获取每个页的数据,然后一条条的推动的oms系统。如果遇到某一条推送失败,那么进行重试两次,如果还是不行则continue。
在后台获取商品详情信息的时候,使用CompletableFuture进行异步编排,可能有一些任务是需要其他任务先完成的,可以使用then方法,就是上一个任务执行完了然后再指向下一个任务。使用allOf收集所有的任务,然后使用get等待所有的线程执行完毕。
线程池
corePoolSize 核心线程数
maximumPoolSize 最大线程数
存放消息的队列:
1.无界队列(例如LinkedBlockingQueue ),如果选择了这个无界队列那么最大线程数就会失效,因为最大线程数是在队列满了之后才会生成的
2.有界队列(例如ArrayBlockingQueue ),如果选择了有界队列那么就要考虑队列大小和最大线程数的大小和拒绝策列了
3.不存储元素的队列(SynchronousQueue),它是要等待接收到的一个任务执行完之后才会再去接一个任务,所以可能需要maximumPoolSize需要是无边界的,保证不会出现拒绝策略。
拒绝策略:
1.abortPolicy,如果队列满了,那么直接抛出异常,可以尽快的感知执行的任务出错了
2.discardPolicy,直接丢弃任务且不会抛出异常,不重要的任务会使用
3.discardOidesPolicy,丢弃队列最前面的任务,然后重新提交被拒绝的任务,看业务有无需要保证执行新的任务需求
4.callerRunsPolicy,由提交任务的线程处理该任务,很重要的任务,需要确保肯定能执行。
如何合理设置这些参数
大概就是核心线程数设置为cpu数+1 为cpu型的
io型的则设置为cpu的两倍
目前项目中配置的是核心线程数是cpu数的两倍 因为需要查询数据库,所以我们判断为io型的程序
最大线程数为核心线程数的两倍
有界队列容量为1000
拒绝策略为callerRunsPolicy确保任务会被执行
我也不懂0.0
https://zhuanlan.zhihu.com/p/123328822
接下里可能会提问的就是synchronized 和 volatile 的理解如果面试官问的很广泛就把自己知道的所有知识点回答上来
synchronized 和reentrantLock的区别
搞懂类似这样的题目:一个文件很大存在十亿个数字,如何找出重复最大的数字(https://hanquan.blog.csdn.net/article/details/108277388)
top k 问题:
先在所有数据(m个)取k个值建立一个小顶堆(就是root节点为最小值的一个数组的数据结构,每个节点都小于子节点)如下图,然后遍历k-m剩下的数据,判断是否大于堆顶,如果大于则插入堆内,然后调整结构,如果小于则不用理会这个数。
在10亿个数字中找出重复最多的数?
使用位图,位图实现就是一个byte[] 数组,相当于一个二维数组,结构如图
通过判断该数字在bitmap中是否已经有值了,如果没有则改为1,如果有则统计该数重复次数+1,最总统计得到重复最多的数。
位图的位置判断,先用 number(数字)/ 8 等到该数字在byte数组中的第几个byte,然后 number(数字)% 8得到在这个byte中的那个bit位上。
添加和查找操作可以看这篇文章:https://zhuanlan.zhihu.com/p/94818952
准备一些es的一些知识点
index(索引):相当于mysql中的数据库
type(类型):相当于mysql中的表
document(文档):相当于mysql中的一行数据
倒排索引:
就是会把一些长的句子分成一个个单词,然后记录每个单词在那个长的句子中出现,保存为一个list,如果数据多的话会变成一个bitmap(位图)。
大概结构如上图,会有term index(字典树)能更快的找到term dictionary(字典)中的term(单词),找到了单词之后就可以知道单词中存在那些document(文档)中。
主要的数据类型:
字符串类型:text 主要是作为需要分词的match时是对text类型字段进行匹配,keyword主要是做等值匹配的。
long,integer
简历上敢写的,都复习。人麻了