JAVA SE
🐱 Final 关键字
final 关键字修饰数据,包括成员变量和局部变量,该变量只能被赋值一次且它的值无法被改变。对于成员变量,对于类变量声明时就要初始化,对于实例变量,必须在声明时或者构造方法中对它赋值。
- 对于基本类型,final 使数值不变;
- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
修饰方法参数:在变量的生存期中它的值不能被改变。
修饰方法:表示该方法无法被重写。
修饰类:表示该类无法被继承。
🐱 Static 关键字
修饰变量:类加载进方法区,所以被static 修饰的变量是共享的。
修饰方法:工具类的方法,不需要建立对象,直接使用类名.方法名的方式调用。
修饰静态代码块:只会在类初始化时运行一次,可以用来执行初始化等操作。
静态内部类:一般方法可以访问静态方法但是静态方法必须访问静态方法。
🏠 你了解不了解深拷贝和浅拷贝
如果拷贝一个对象的时候只对基本数据类型进行一个拷贝,对引用类型只进行一个引用的传递,没有真实的创建一个对象的化这是一个浅拷贝,反之,如果真的创建了一个这样的对象,并且复制了其中的成员变量就可以认为是一个深拷贝。这样的话,如果我们想实现一个深拷贝,我可以想到的方法就有两个,一个是在引用对象的拷贝时,对其内部的引用变量进行一次clone()。第二种就是借助序列化,我们可以将对象先进行序列化,在对其进行一个反序列化,只是需要我们对序列化的规则进行一个重写罢了。序列化的方式比较慢,但是易于维护。
🚀 什么是序列化和反序列化
序列化和反序列化是我们经常用到的,一般来说,我们可以将pojo类全部设为可以序列化的类。通过使用序列化的方式,可以让存在于JVM内存区域的对象进行一个持久化,序列化到流中,还可以进行网络传输。有些时候,是必须支持序列化的。我们在RMI或者RPC调用中,传入和返回的参数都是需要序列化的,所有需要保存在磁盘中的java对象都是必须进行序列化的。
🌂String StringBuffer StringBuilder
一般来说,这三个都是可以进行字符串操作的api,是不过String是不可变的,StringBuilder 和 StringBuffer 是可变的, String的不可变是因为其里面的byte数组是private final 修饰的。而另外两种的可变在于他们内部的数组不是final修饰的。
String 为什么要被final 修饰,这样做的原因就在于可以实现字符串池,并且这种不变性天然具有线程安全性和网络传输的安全性,并且这样 hashcode 在string创建时就被缓存了,不需要重新计算。
另外两种可变的类,他们的区别在于StringBuffer内部的append操作被Synchronized修饰,是线程安全的,StringBuilder线程不安全。
🐟 JAVA 的反射机制
JAVA反射机制就是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法。对于任意一个对象都可以调用它的任意方法和属性,并且能够改变它的属性。·JAVA的反射是通过 Class 对象和 reflect包下的函数相配合使用的。像是在项目中使用Class.forName()的副作用进行加载一个类,初始化class。通过反射运行配置文件内容,实现一个解耦。通过反射越过泛型检查,泛型是在编译期间起作用的。在编译后的.class文件中是没有泛型的。所有比如T或者E类型啊,本质都是通过Object处理的。所以可以通过使用反射来越过泛型检查。还有RPC远程调用的时候,在接收方使用反射获得参数的类型,调用的方法等信息。此外Spring中的AOP就是反射加动态代理实现的。
🦈JAVA 集合类
JAVA 的话有两种集合:Collection接口和Map接口的,Map接口针对的是键值对。实现Collection接口的有List和Set List接口下主要有ArrayList vector LinkedList,Set接口下主要有HashSet TreeSet,然后Map应该是使用比较多的,实现Map接口的有HashMap,TreeMap,LinkedHashMap,这个可以用来做LRU。还有WeakHashMap;一般使用比较多的就是HashMap和ArrayList,像商城中一些参数的传递可以使用Map,商品id和商品信息这种键值对格式的使用hashmap是非常合适的,而且像一些中间件 如Redis 就大量的使用了HashMap的思想。而像前端的VO商品信息,就可以用List这种形式进行接收。
使用 ArrayList 的很大一部分原因在于 ArrayList 实现了 RandomAccess 接口,可以进行一个随机访问,查询速度快。而且对于在尾部的增删操作是一个O(1),但是对于在中间位置的操作,因为增加删除都需要把后面的元素进行位置移动,时间复杂度在O(n-i)。
ArrayList 的话底层使用的是数组,初始容量是10,当真正向其内部添加元素的时候才真正分配容量。ArrayList是可以动态扩展的,当我们插入一个元素的时候,首先会检查ensurecapacity,可以的话就加到末尾,空间不足的话就触发扩容机制,扩容大概是1.5倍。因为ArrayList的动态扩容机制,所以可能会产生一个内存浪费的问题,所以使用的时候可以赋初值的方式尽量避免扩容。
LinkedList底层是双向链表,它的好处和ArrayList相比就是数组和链表的区别,便于增删,不能进行随机访问,查询较慢。因为使用的是链表所以额外的内存开销就在于结点会多存取next引用。使用LinkedList的话,一般不是很多,但是它可以实现队列。
vector ,是一个线程安全的List容器,它和ArrayList类似,但是现在不太建议使用。一般我们可以使用Collection.synchronizedList方法或者是CopyOnWriteArrayList。
HashMap的话,是使用较多的一个类,而且应用也比较广泛。其内部是一个entry数组,实例化一个hashmap的时候,会默认会创建一个长度为16 的数组,通过链地址的方式来解决hash冲突,链表结点就是entry,里面有hash 值,key value next引用。hashmap 是有大更新的在1.8的时候,bucket里面的链表长度大于8的时候会发生树化,变成红黑树,在减小到小于6的时候就会重新成链。
Set 接口常见的实现类
HashSet: HashSet 的底层实现是一个HashMap, 只是add的时候只使用了HashMap的key部分
TreeSet: 可以对Set集合进行排序,默认是自然排序
TreeSet 存放自定义类型如何实现有序?
分别是采用 Comparable 接口和 Comparator 来实现
Set 集合的特点就是无序,不可重复 对比List集合就是有序的可以重复(按照插入顺序有序)
public class Student implements Comparable{
int age;
String name;
...// setter and getter
@override
public int compareTo(Object o){
if(!(o instanceof Student)){
throw new RuntimeException("o 参数不合法");
}
return ((Student)o.age - this.age );
}
}
public class test{
public static void main(String []args){
Student zs = new Student(20, "zs");
Student ls = new Student(30, "ls");
Set set = new TreeSet();
set.add(ls);
set.add(zs);
for(Iterator it = set.iterator();it.hasNext();){
Student s = (Student) it.next();
System.out.println(s.name + s.age);
}
}
}
public class Person {
int age;
String name;
...// setter and getter
}
public class Person{
public static void main(String []args){
Person zs = new Person(20, "zs");
Person ls = new Person(30, "ls");
Set set = new TreeSet(new Comparator(){
@Override
public int compare(Object o1, Object o2){
if(!(o1 instanceof Person)){
throw new RuntimeException("o1 参数不合法");
}
if(!(o2 instanceof Person)){
throw new RuntimeException("o2 参数不合法");
}
}
});
set.add(ls);
set.add(zs);
for(Iterator it = set.iterator();it.hasNext();){
Person s = (Person) it.next();
System.out.println(s.name + s.age);
}
}
}
Java 异常处理
项目中是如何处理异常的?
通常是使用 try catch 的方式进行处理的,有时候也会抛出交给上层处理,在项目中还使用过自定义异常,自定义异常继承自 RuntimeException 当然也可以继承 Exception 但是两者有本质的区别。
异常的分类(Java 中异常处理的逻辑)
Error: Java 运行时系统的内部错误和资源耗尽错误,应用程序不会抛出该类对象。
Exception: 受控异常 和 非受控异常。
受控异常:直接继承自Exception 的是受控异常一般是外部错误,这种异常发生在编译阶段,Java编译器会强制程序去捕获此类异常。
非受控异常: RuntimeException 的直接子类,是那些可能在Java虚拟机正常运行期间抛出的异常的类。
在工作中异常的处理?
异常主要是通过抛出和捕获进行处理
抛出异常三种方式,分别是throw throws 和 系统自动抛出异常
throw 用在方法内,后面跟的异常对象,执行到throw语句以后方法就结束了,一定抛出异常
throws 用在方法上,后面跟的是异常类,可以又多个,用来声明异常,让调用者知道该功能可能出现问题,可以给出预先处理方式
throws 声明的是异常的可能性,不一定会发生这些异常
捕获异常: try catch 处理异常 只有对应的try 语句得到执行的情况下,finally语句才会执行
@ControllerAdvice
🏥 Java 的 IO 操作
🦀工作中如何做JAVA的IO流操作的
JAVA 的IO操作从方向上可以分为 inputStream 和 outputStream。从单位上有字节流和字符流,从单位上可以通过StreamReader转换为字符流,为了提高效率也用到buffer流。以上介绍的流操作都是BIO流,同步阻塞IO模式,数据的读取和写入必须阻塞在一个 线程内等待其完成,项目中直接操作IO的业务场景不是太多。但是我了解到很多分布式框架底层的通信都是使用NIO流,NIO流最核心的组件就是Buffer Channel 和 Selector是面向缓冲的基于通道的同步非阻塞IO模型。NIO的话,类似于操作系统中的IO多路复用,使用selector去监听多个channel,当channel中感兴趣的事件发生,selectKey被选中,就可以对其进行处理。Java1.7的时候是引入了AIO的,异步IO基于事件和回调机制实现的,应用程序操作时候会直接返回,不会阻塞,这和操作系统的异步IO是一致的,当后台处理完之后,操作系统会通知应用进程进行处理。但是据我了解,AIO的使用很少。
java 的数据流
文件通常是由一连串的字节或字符构成,组成文件的字节序列称为字节流,组成文件的字符序列称为字符流。java 中根据流的方向可以分为输入流和输出流。
输入流是将文件或其它输入设备的数据加载到内存的过程,输出流是将内存中的数据保存到文件或其它输出设备。
对BIO的理解
BIO: 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时,服务器开一个线程与其连接处理
实现方式
传统 BIO 采用BIO通信模型的服务端,通常由一个独立的 acceptor 线程负责监听客户端的连接,无客户端连接即阻塞。可以通过多线程来支持多个客户端连接。
改进
伪异步IO
public class BIODemo{
public static void main(String[] args){
// 定义一个服务端
ServerSocket serverSocket = null;
try{
serverSocket = new ServerSocket(7777);
// 监听机制,当有新的连接那么需要创建一个线程
while(true){
Socket socket = serverSocket.accept(); // 阻塞的
// 有连接
new Thread(){
// 处理客户端的请求
public void run(){
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(),true);
String str = "";
while(true){
if(str = in.readLine() == null) break;
System.out.print("服务器端接受到的数据是" + str);
}
}
}.start();
}
}catch(Eexception e){
}finally{
// 关闭相关资源
}
}
}
NIO: 同步非阻塞,服务器实现模式,一个线程可以处理多个请求。
NIO的Buffer:容量 缓冲区的当前终点,下一个要读或写的位置,标记
NIO的Channel: FileChannel DatagramChannel SocketChannel ServerSocketChannel
NIO Channel和流的区别? Channel是双向的,可读可写;流是单向的,Channel可以进行异步读写,Channel是基于Buffer 的。
Selector: Java NIO 中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样一个单独的线程可以管理多个Channel,从而管理多个网络连接。Selector是一个抽象类,定义了一些主要的方法。有一个具体的实现SelectorImpl。
public class NIODemo{
public static void main(String[] args){
FileInputStream fileInputStream = new FileInputStream(“1.txt”);
FileChannel inChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream(“2.txt”);
FileChannel outChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将文件1内容读到Buffer中并写到文件2中
while(true){
byteBuffer.clear();
int read = inChannel.read(byteBuffer);
if(read == -1) break;
// 将buffer内容写到2
byteBuffer.flip();
outChannel.write(byteBuffer);
}
}
}
AIO: 异步非阻塞,AIO引入了异步通道概念,采用Proactor模式,简化了程序编写,有效的请求才启动连接。
网络通信
TCP IP 协议
在浏览器中输入url地址 显示主页都经历了什么
1 DNS 服务器将域名解析为 IP 地址返回给主机
2 主机与服务器请求建立 TCP 连接
3 主机向服务器发送 HTTP 请求 服务器向客户端发送 HTTP 响应,客户端根据响应渲染页面
4 客户端向服务器发送断开连接
使用协议
TCP : 建立连接
IP: 网络层发送数据
ORSF:路由选择协议
ARP:IP地址转换为MAC地址
HTTP:访问网页
网络分层模型的认识
网络分层是将网络节点所要完成的数据处理工作,分别由不同的硬件和软件模块去完成。
这样可以将通信和网络互联这一复杂问题变得较为简单。
常规的网络分层模型有四层、五层和七层。
TCP 和 UDP区别
-
用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。
-
传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。
TCP 如何保证可靠传输
TCP 分割数据段,进行包编号,首部有校验和,对于重复数据会有丢弃操作,有流量控制和阻塞控制,ARQ协议以及超时重传。
TCP 的三次握手
准备阶段:TCP服务器端创建TCB(传输控制块)进行监听客户端连接(被动打开),客户端主动建立TCP(主动打开)
1 客户端发送 SYN = 1, seq = x ;
2 服务器端发送 SYN = 1, ACK = 1, seq = y, ack = x + 1;
3 TCP客户端发送确认 ACK = 1, seq = x+1, ack = y+1;
4 建立连接
为什么要三次握手
为什么要SYN
为什么还要ACK
TCP 的四次挥手
1 客户端主动关闭 向服务器端发送 FIN = 1 seq = u,服务器通知应用进程,
2 服务器向客户端发送 ACK = 1 seq = v ack = u+1 服务器端进入close-wait 被动关闭
3 服务器数据传输完毕后向用户发送 FIN = 1, ACK = 1, seq = w, ack = u+1
4 客户端向服务器发送 ACK = 1, seq = u+1, ack = w + 1 客户端进入 time-waited 等待 2msl进入closed
服务器收到后进入closed
HTTP 是如何保持长连接的
HTTP的长连接指的是通过三次握手建立起一次连接,可以在连接内发送多次request请求。
http 1.0 协议是短连接 http 1.1默认是长连接 响应头中加入了connection:keep-alive
request 的结构
request 由请求行 请求报头 空白行 和 请求体
请求行中的请求方法:GET POST PUT DELETE HEAD TRACE CONNECT OPTIONS
GET 和 POST 区别
传参位置 GET url 后面 POST 请求体
参数大小 GET 有限制 POST 可以认为是没有限制的
是否缓存 GET 默认缓存 POST 不缓存
使用场景 GET 是读 POST 是写
RESPONSE
response 包括状态行 响应报头 空白行 响应正文
状态码
状态码:
- 1XX 请求已接收,继续处理
- 2XX 请求被成功接收 处理 接受
- 3XX 重定向
- 4XX 客户端错误
- 5XX 服务器端错误
200 OK 请求成功
400 Bad Request 客户端请求有语法错误,不被服务器端处理 大多数是传参错误
401 权限
403 拒绝服务
404 资源不存在
500 服务器发生不可预期错误
503 服务器当前不能处理客户端的请求,一段时间可能恢复正常
不同的HTTP协议对比
HTTP 1.0 1.1 长连接 错误状态响应码 缓存处理 带宽优化及网络连接的使用
上线的项目使用的是http?
线上系统一般使用的是https,安全性更高
区别 端口:http 80 https 443
安全性:https 安全一些 http 不太安全
性能损耗:https 性能损耗更多一些
Jsp和servlet
servlet是特殊的java程序,能够以来web服务器的支持向浏览器提供显示内容
jsp本质上是servlet的一种简易形式,jsp 会被服务器处理成一个类似于servlet的Java程序,可以简化页面内容的生成
jsp侧重于视图,servlet侧重于控制逻辑
Servlet
Servlet 本质是服务器端的java程序,必须实现SUN制定的javax.servlet.Servlet接口
生命周期:Servlet 的生命周期始于将它装入内存时,并在终止或重新装入Servlet时结束。包括加载和实例化初始化处理请求和服务结束。
这个生存期由javax.servlet.Servlet接口的init service destroy 方法表达。
Servlet 是线程安全的吗?
servlet是单例的,对于所有请求都使用一个实例,如果有全局变量被多线程使用的时候,就会出现线程安全问题。
解决这个问题 singleThreadModel synchronized 不适用全局变量
Forward 和 Redirect 区别
转发是服务器行为,重定向是客户端行为
转发是一次请求,重定向是两次请求
转发共享request数据,重定向不共享
转发效率高,重定向效率低
Session 和 Cookie
怎么理解http 是一种无状态协议
http无状态协议,是指协议对于事物处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则必须重传,这样可能导致每次连接传送的数据量增大。
浏览器首次访问web服务器的时候,会建立一个连接,服务器会生成一个cookie 一个session对象 cookie对象Jsessionid : 1678 Session对象 key(Jsessionid)value(session内容),session对象存在服务器中,cookie对象响应给客户端浏览器,当再次访问的时候,请求中会携带cookie信息,因此服务器可以访问上次存放的session。
区别:Session 存在服务器端,Cookie存在客户端。Session安全性高一点。服务器端的Cookie浏览器默认将其保存在缓存中,当浏览器关闭之后Cookie消失
服务器端创建Cookie时可以通过setMaxAge()方法设置Cookie的有效时间。
Cookie 禁用的情况下,使用所有的访问路径url后面,带一个Jsessionid。
Mysql的架构模型
Mysql底层是如何实现的
客户端-> 连接器 -> 分析器 -> 优化器 ->执行器
连接器:使用连接池
查询缓存:Mysql 8.0 之后删除了
分析器:词法分析 语法分析
优化器:使用它认为最优的方式执行 explain
执行器:调用存储引擎接口最终执行
存储引擎
数据库存储引擎是数据库底层软件组织,DBMS使用数据引擎进行创建查询更新删除数据
Mysql5.5之前默认存储引擎是MyISAM,在此之后默认存储引擎是InnoDB。Mysql SHOW ENGINES查看内置存储引擎。
存储引擎是表级别的,可以修改。Innodb MyISAM MEMORY
MyISAM :由frm(表结构,字段) myd(数据) myi(索引)三个文件组成,不支持事务,提供表级锁,不支持外键,可以被压缩节省空间,适用于读场景
InnoDB :由frm(表结构)idb(数据和索引) 支持事务,提供行级锁,可以通过行锁实现表锁,支持外键
Memory :定义在frm(表结构)文件中,数据存放在内存中,访问效率高,提供表级锁。
事务
数据库的事务具有ACID特性,其中I就是隔离性;数据库存在RU、RC、RR和 S四种隔离级别,设置不同的隔离级别有可能产生脏读,不可重复读,幻读等不同的
数据一致性问题。事务的隔离级别是使用LBCC(锁)机制和MVCC(多版本控制)机制来实现的。
什么是事务?特性
原子性:整个事务的所有操作必须作为一个单元全部完成,借助undolog实现
一致性:在事务开始之前与结束之后,数据库都保持一致状态。一般使用业务逻辑来保证。
隔离性:一个事务不会影响其他事务的运行,使用锁机制和MVCC机制来实现。
持久性:事务完成之后,该事务对数据库所作的更改将持久的保存在数据库中,并不会被回滚了。
事务的隔离级别产生的数据一致性问题
读未提交: 脏读 不可重复读 幻读
读已提交: 不可重复读 幻读
可重复读: 幻读(Innodb下不会出现幻读问题)
串行化 :
读未提交:出现脏读问题
脏读:读到另一个事务未提交的内容
tx:1
set session transaction isolation level read uncommitted;
select @@tx_isolation;
select * from emp where empno = 7369;
tx:2
begin;
update emp set sal = 800 where empno = 7369;
tx:1
select * from emp where empno = 7369;
tx:2
rollback;
tx:1
select * from emp where empno = 7369;
读已提交:解决了脏读问题,但是没有解决不可重复读的问题,还可能存在幻读问题
不可重复读:在同一事务中,两次读取同一条数据结果不同;
tx:1
begin;
set session transaction isolation level read committed;
select @@tx_isolation;
select * from emp where empno = 7369;
tx:2
begin;
update emp set sal = 800 where empno = 7369;
commit;
tx:1
select * from emp where empno = 7369;
幻读:一个事务按照相同条件读取以前检索过的数据时,发现了其他事务插入的新数据
幻读和不可重复读区别是:不可重复度的重点是修改,幻读的重点在于新增或者删除。
tx:1
set session transaction isolation level read committed;
begin;
select @@tx_isolation;
tx:2
begin;
insert into 'emp' values(5,'smith','clerk',7902,'1999-09-09',700,null,20);
tx:1
select * from emp ;
tx:2
commit;
tx:1
select * from emp ;
可重复读,解决不可重复度和幻读问题
tx:1
set session transaction isolation level read repeatable;
select @@tx_isolation;
begin;
tx:2
begin;
insert into 'emp' values(5,'smith','clerk',7902,'1999-09-09',700,null,20);
tx:1
select * from emp ;
tx:2
commit;
tx:1
select * from emp ;
LBCC 实现隔离性
隔离性的实现之一是使用锁机制LBCC(LOCK BASEDCONCURRENCY CONTROL),读取数据之前对其加锁,阻止其他事务对数据进行修改;
Mysql支持哪些锁:
粒度:表锁 行锁 页锁
类型:共享锁 排他锁 意向锁(意向共享,意向排他)
用法:乐观锁 悲观锁
算法:临键锁 间隙锁 记录锁
共享锁:多个事务对于同一数据可以共享一把锁,都可以访问到数据,但是只能读不能修改,因此被称为读锁 S锁
查询语句后面lock in share mode
tx:1
commit;
begin;
select * from emp where empno = 7369 lock in share mode;
tx:2
commit;
select * from emp where empno = 7369;
update emp set sal = 900 where empno = 7369; // 光标会一直停在这
tx:1
rollback; // 事务2 会执行成功
排他锁:不能与其他锁共存,又称写锁,X锁。更新自动加排他锁、查询语句使用for update
tx:1
begin;
update emp set sal = 1000 where empno =7369;
tx:2
begin;
select * from emp where empno = 7369 lock in share mode;//光标会一直在这
ctrl c
select * from emp where empno = 7369 for update; // 光标会一直在这
ctrl c
select * from emp where empno = 7369; // 会出现结果?
锁的本质
Innodb 支持的是行锁,是通过给索引上的索引项加锁来实现的,只有通过索引条件进行数据检索,InnoDB才使用行锁否则将使用表锁,锁住
索引的所有行,类似于表锁。
Innodb行锁使用的是什么算法实现的
Innodb 默认的行锁使用的临键锁算法,不止把自己的区间锁住,还把下一个临键的区间锁住,就能够防止幻读。
其他:记录锁 间隙锁
当sql执行按照索引进行数据检索时,查询条件为范围查找(between and, <, >等)并有数据命中,这时sql语句加的锁为next-key-locks 锁住了索引记录所在区间以及索引记录的下一个区间(左开右闭),没有查到结果的话就退化为间隙锁或记录锁。
间隙锁:sql 按照索引进行数据检索,查询条件为范围查找没有数据命中 或者(等值查询并有数据命中),在RR隔离级别
记录锁:使用唯一性索引(主键索引和唯一索引),条件为精准匹配从表中检索数据时,可以命中唯一一条记录。
MVCC 实现隔离锁
一个事务加了排他锁,为什么其他事务还是能够读取到数据?
MVCC:事务中第一次读取数据时,生成了一个数据请求时间点的一致性数据快照,并用这个快照来提供语句级或事务级的一致性读取。
数据库中每一个表都存在三个隐藏的字段,MVCC是通过隐藏字段来完成的。
DB_ROW_ID: 行ID,占用6个字节,当没有主动给表设置主键也没有设置唯一索引时,那么自动给这个字段加了索引。
DB_TRX_ID: 记录insert或update的事务ID,6个字节。每当有insert或者update数据前需要拿到当前事务ID,将这个事务ID记录到此列。
DB_ROLL_PTR: 记录delete的事务ID,7个字节。每当delete数据时需要拿到当前事务ID,将这个事务记录在此列。
数据库的优化
在查询中exists 和 in 哪个效率高
in 是把外表和内表做hash连接,而exists是对外表做loop循环,每次loop循环再对内表进行查询。如果查询的两个表大小相当,那么用in和exists差别不大。如果两个表中一个较小,一个是大表,则子查询大的用exists,子查询小的用in。
数据库结构优化
调整过数据库的参数吗?
通过对mysql参数调整可以提高资源的利用率,从而达到提高mysql服务器性能的目的。mysql的配置参数再my.conf 或者 my.ini文件中。
数据库的设计优化
设计数据库时需要遵循哪些原则
三大范式
第一范式:有主键,具有原子性,字段不可分割。
第二范式:完全依赖,表中非主键列不存在对主键的部分依赖。要求每个表只描述一件事情。
第三范式:没有传递依赖,表中的列不存在对非主键列的传递依赖。
列选择原则
字段类型优先级:整型>date time>char varchar>blob
长度够用就行
尽量避免使用null
非负的数据,优先使用无符号存储
财务数据必须使用decimal类型
反范式设计
explain
使用explain关键字可以模拟优化器执行sql查询语句,从而知道mysql是如何处理你的sql语句的,分析你的查询语句或是表结构的性能瓶颈。
分析出的结果?
表的读取顺序
数据读取操作的操作类型
哪些索引可以使用
哪些索引被实际使用
表之间的引用
每张表有多少行被优化器查询
explain select * from tb_item
索引
在查询中in和or哪个效率高
给in和or的效率下定义的时候,应该考虑一个前提条件:所在的列是否有索引。如果有索引性能没啥区别,如果没有索引,in的效率会比or有着明显的提高。
索引的本质和作用
索引本质是数据结构,这种数据结构能够帮助我们快速的获取数据库中的数据。有了索引相当于我们给数据库的数据加了目录一样,可以快速的找到数据,如果不使用索引则需要一点一点的去查找数据,简单的说提高数据查询的效率。
优点
可以通过建立唯一索引或者主键索引,保证数据库表中每一行数据的唯一性。
建立索引可以大大提高检索的数据,以及减少表的检索行数,提升效率
在分组和排序字句中进行数据检索,可以减少查询时间中分组和排序所消耗的时间
缺点
在创建索引和维护索引会消耗时间,随着数据量的增加而增加。
索引文件会占用物理空间。
当对表的数据进行更新操作的时候,索引也要动态的维护,这降低数据的维护速度。
如果你在一个大表上创建了多种组合索引,会造成索引文件的膨胀。
索引的原理
建立索引增加了检索速度,索引其实是一颗B+树。B+树的度和关键字个数是相等的。B+树中的非叶子节点不存储数据,只存储键值。叶子结点没有指针,所有的键值都会出现在叶子结点上。每个非叶子结点由n个键值key和n个指针point组成,即和关键字是相等的。
聚集索引和二级索引
聚集索引:决定了数据的物理存储顺序的索引。一个表一定要有且仅有一个聚集索引。
如果创建了主键索引,那么主键索引就是聚集索引
如果没有主键索引,第一个uniquekey索引就是聚集索引
如果也没有uniquekey索引,那么将表中的一个内置的隐藏rowid字段作为聚集索引。
二级索引:所有的非聚集索引都是二级索引。
MyISAM
MyISAM 引擎的表由三个文件组成,分别是frm myd myi。myi文件存储的就是索引文件,MyISAM 引擎的索引结构下,主键索引和二级索引是一致的,叶节点的data域存放的是数据记录的地址。
Innodb
InnoDB 分为主键索引和二级索引,二者的存储结构是不同的。MyISAM索引文件和数据文件是分离的,索引文件仅保存在数据记录的地址。在InnoDB中,表数据文件就是按照B+树组织的一个索引结构(聚集索引),这棵树的叶节点data域保存了完整的数据结构。索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引,存放到idb文件中。二级索引中叶子节点中存放的是主键的值。
索引优化技巧
离散度:count(distinct(column))::count(*)离散度在不超过全表10%-15%的前提下索引才可以显示其所具有的价值,当离散度超过该值的时候全表扫描可能比索引扫描更加高效。
最左前缀原则:主要体现在联合索引上,最左前缀原则指的是查询的时候,查询条件可以从左到右精确匹配连续的一列或几列就可以命中索引。
左前缀不易区分列如何加索引
(例如:url:)倒序加索引。
JVM
Java 跨平台性实现
编译器将源文件编译成字节码文件。classLoader将字节码文件转换为JVM中的Class对象,JVM利用Class对象实例化出对象。
类加载和双亲委派机制
public class SamueClassLoader extends{
public CLass findClass(String name){
byte[] b = loadClassData(name);
return defineClass(name, b.)
}
}
Redis
🐟 Redis 某个key设置过期时间,是到了这个时间内存就会被回收吗?
Redis 的某个key虽然设置了过期时间,但是并不是到了时间就马上进行内存回收,这涉及到了Redis的过期删除策略和内存回收机制。
NoSQL
NoSQL:非关系型数据库,Not-Only SQL 作为关系型数据库的良好补充。
满足对数据库高并发读写的需求,对海量数据的高效率存储和访问的需求,对数据库高扩展性和高可用性的需求。
NoSQL 分类
键值对存储数据库: Redis memached 典型应用:内容缓存,主要用于大量数据的高访问负载,数据模型是一系列键值对,可以进行快速查询,但是存储的数据缺少结构化
列存储数据库:HBase 典型应用:分布式的文件系统 bson 数据模型是以列簇式存储,将同一列数据存在一起,查找速度快,可扩展性强,更容易进行分布式扩展,但是功能相对局限
文档型数据库 MongoDB 典型应用:Web应用数据模型是一系列的键值对,对数据结构的要求不严格,但是查询性能不高,缺乏统一的查询语法
图形数据库:InfoGrid 典型应用:社交网络 数据模型是图结构,可以利用图结构的相关算法,但是需要对整个图做计算不容易做分布式的集群方案。
为什么使用 redis 而不是 map
map 只能存在于jvm中,受到堆空间大小的限制,持久化只能进行序列化
redis 有持久化,用在分布式中更多,可以有主从可以有集群,作为单独的服务不受堆空间大小的限制
Redis 和 memached
redis : 支持五种数据类型,查询操作支持批量操作,支持事务处理,redis 的网络模型是单线程的,有简易的AEEvent,支持持久化
memached: 支持一种数据类型(文本 新增了二进制类型),支持的操作少,多线程的,libEvent,不支持持久化。
Redis 常见的数据结构
String hash set list zset
String:set,get,decr,incr,mget String 数据结构是简单的key-value(value可以是String可以是数字),应用:常规计数:微博数,粉丝数
Hash :hget,hset,hgetall Hash是一个string类型的filed和value的映射表,Hash特别适合用于存储对象,后续操作的时候,你可以仅仅修改这个对象中的某个字段的值。比如我们可以用Hash来存储用户信息,商品信息。
List :lpush,rpush,lpop,rpop,lrange list的实现是一个双向链表,所以可以支持反向查找和遍历,不过带来了额外的内存开销。另外可以通过lrange命令,就是从某个元素可以读取多少个元素,可以基于list实现分页查询,基于redis实现简单的高性能分页。
Set :sadd, spop, smembers, sunion set对外提供的功能于list类似,是一个列表的功能,可以存放不重复的数据,set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list不能提供的,可以基于set实现交集并集差集的操作。
Zsort:zadd,zrange,zrem,zcard 和set相比,有序集合增加了一个权重参数score,使得集合内的元素能够按照score进行有序排列。在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜等信息。
Redis 的过期时间
1 定期删除:redis 默认是每隔100ms 就随机抽取一些设置了过期时间的key,检查是否过期,如果过期就删除。
2 惰性删除:如果你的 key 过期了,使用定期删除没有成功,那么查一下那个key的时候,才会被redis删除掉。
3 内存淘汰机制:内存淘汰策略通过配置文件中的maxmemory-policy noeviction 来配置,redis提供了6中数据淘汰策略
voilatile-lru volatile-ttl volatile-random allkeys-lru(最经常使用) allkeys-random no-eviction
Redis 的持久化机制
1 RDB Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点的副本。
save 900 1 上一次快照到现在有 900s 至少有1个键发生变化就需要 RDB
save 300 10 300s
save 60 10000 60 s
2 AOF 开启AOF持久化后,没执行一条会更改Redis中数据的命令,Redis就会将该命令写入硬盘的AOF文件中
appendfsync always 每次有数据修改变化发生时都会写入AOF文件,这样会严重降低redis 的速度
everysec 每秒一次(常用,兼顾数据和写入性能)
no 让操作系统决定何时进行同步
在主从服务器模型下主服务器一般使用RDB从服务器使用AOF
使用Redis应该注意的问题
redis 使用不当的话会造成很多严重的问题,如缓存穿透,雪崩还要考虑缓存和数据库双写时的一致性问题。
缓存穿透:一般指故意请求缓存中不存在的数据,导致所有的数据都落在数据库上,造成数据库短时间承受大量请求而崩掉。
解决: 使用布隆过滤器,将所有可能存在的数据,哈希到一个足够大的bitmap中,一定不存在的数据会被bitmap拦截掉,从而避免了对底层存储系统的查询压力。如果一个查询返回的数据为空,我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
雪崩: 缓存同一时间大面积失效,后面的请求都会落到数据库上,造成数据库短时间内承受大量的请求而崩掉。
解决:尽量保证redis集群的高可用性,发现机器宕机尽快补上。本地ehcache缓存+hystric限流&降级,避免mysql崩掉,利用redis持久化机制保存的数据尽快恢复缓存。
双写一致性:如果系统不是严格要求缓存+数据库必须强一致性的话,可以采用cache-aside pattern。
如果强一致性,那么可以使用读请求和写请求串行化,串到一个内存队列中,使用串行队列会使吞吐量大幅降低。
主从复制和集群
主从复制的好处
没有单点故障
可以读写分离
主从复制的过程
从服务器向主服务器发送sync命令
主服务器后台启动fork保存快照RBD,在保存RBD的过程中,接受到的所有写命令全部缓存。
主服务器将RDB和写缓存发送给从服务器
从服务器恢复RDB数据并且执行写的那些缓存命令。
主服务器接收到的所有写命令都发给从服务器。
哨兵机制
哨兵的作用就是对Redis 的系统运行情况进行监控,他是一个独立的进程,他的功能:监控主数据库和从数据库是否运行正常,主数据库出现故障后自动将从数据库转化为主数据库。
哨兵的配置文件sentinel.conf(sentinel monitor e3Master 127.0.0.1 6379 1)
redis 的集群
即使有了主从复制,每个数据库中都要保存整个集群中所有的数据,容易形成木桶效应,所以还需要集群。
集群中Hash槽和key的关系,key的有效部分使用CRC16算法计算出哈希值,再将哈希值对16384取余,得到插槽值。
有效部分:如果key中包含了成对的大括号并且{}之间存在字符,那么有效部分指的是{}之间的部分,否则整个key都是有效部分。
redis 是如何得知其他节点是否正常呢?
集群中的每一个节点都会向其它节点发送PING命令,通过有没有收到回复判断目标节点是否下线。集群中每一秒就会随机选择5个节点,然后选择其中最久没有响应的节点发送PING命令。如果一定时间内目标节点都没有响应,那么该节点就认为目标节点疑似下线。
当集群中的节点超过半数认为该目标节点疑似下线,那么该节点就会被标记为下线。
当集群中的任何一个节点下线,就会导致插槽区有空挡,不完整,那么该集群将不可用。
解决方式:在redis集群中可以使用总从模式实现某一个节点的高可用,在该节点宕机后,集群会将该节点的slave转变为master继续完成集群服务。
Spring 和 SpringMVC
你是怎么理解Spring的?
Spring是分层的轻量级开源框架,核心是IOC和AOP。
Spring 特点
非侵入式,容器,IOC和AOP的特征
容器:加载配置文件 Bean的生命周期 Bean的作用域 线程的安全性
IOC: 概念 IOC的过程
AOP:定义 实现方式 事务管理 (方式,传播特性,隔离级别)
Spring作为容器,Bean通常在XML中定义,是如何加载XML的
ApplicationContext 接口和 BeanFactory 接口都定义加载配置文件的方法,两者一下区别:
ApplicationContext 容器,会在容器对象初始化的时候,将其中的所有对象一次性装配好,以后使用,只需要从内存中直接获取,执行效率较高,但是占用内存
BeanFactory,对容器中对象的装配与加载采用延迟加载策略,即在第一次调用getBean的时候,才真正装配该对象。
bean 的生命周期
实例化:容器通过BeanDefinition对象中的信息进行实例化,实例化的对象被包装在BeanWrapper对象中
设置属性:BeanWrapper对象提供了设置对象属性的接口。
实现Aware接口
初始化,实行init方法
销毁
Spring创建的Bean是单例的还是多例的
scope属性,为bean指定特定的作用域。spring 支持5中作用域。
singleton:默认,单例。
prototype:原型,每次使用getBean都是一个新的实例。
request: 每次http请求都创建一个bean
session: 对于每个不同的httpSession,都将产生一个bean
global session: 全局的httpSession对应一个Bean实例
Spring 创建的bean是如何处理线程安全的
spring 没有对bean 的多线程安全问题做出任何保证和措施。
不要再bean中声明任何有状态的实例变量或类变量。
如果必须如此,可以使用ThreadLocal 如果需要多个线程之间共享,只能使用锁。
Spring IOC
IOC是一种概念,是一种思想,流行的实现方式是DL和DI,指的是调用者不创建被调用者的实例,而是由Spring容器创建被调用者并注入调用者。
Spring DI:工厂 反射 配置文件
Spring 是如何实现IOC的
在Spring 启动时读取应用程序提供的bean 配置信息。
在Spring 容器中生成一份相应的Bean配置注册表。
根据这张注册表实例化Bean,装配好Bean之间的依赖关系,为上层应用提供准备就绪的运行环境。
其中Bean缓存池是hashMap实现。
Spring 的循环依赖是如何解决的
循环依赖是指两个或者多个bean之间互相持有对方的引用形成了环状的结构。
Spring 对于属性方式的引用可以采用三级缓存机制可以解决循环引用的问题。
Spring 对于构造函数的方式造成的循环引用是不能解决的。
Spring的bean 对象需要实例化再set属性。
第一层缓存:存放的是完完全全创建好的Bean
第二层缓存:存放的是半成品的Bean
第三层缓存,存放的是工厂Bean
Spring AOP 理解
AOP : 面向切面编程就是将交叉业务逻辑封装成切面,利用AOP的功能将切面织入到主业务逻辑中。
面向切面编程是面向对象编程OOP的一种补充,面向对象编程是从动态角度考虑程序的结构,面向切面编程就是从动态角度考虑程序的运行过程。AOP的底层实现就是采用动态代理模式实现的。
Spring是如何实现的AOP
代理类和被代理类是实现共同的接口(或继承)。
代理类中存有指向被代理类的引用,实际执行时通过调用代理类的方法、实际执行的时被代理类的方法。
Spring 提供了两种方式 JDK Proxy 和 Cglib
默认的策略是如果目标类是接口则使用JDK动态代理技术,否则使用Cglib来生成代理。
1 JDK 需要接口
2 Cglib 底层实现是ASM技术
3 JDK 的动态代理一般是要快一些的
Spring AOP 应用场景
权限 缓存 内容传递 错误处理 懒加载 调试 记录跟踪 优化 校准,性能优化,持久化,资源池,同步,事务
Spring 是如何管理事务的
直接调用事务的 API
基于事务代理工厂 Bean 管理 TransactionProxyFactoryBean
基于事务的注解管理 @Transactional
基于Aspectj AOP配置事务
Spring 设置事务的隔离级别
1 Default 采用DB默认的事务隔离级别 mysql rr oracle 提交读
2 read uncommitted
3 read committed
4 repeatable read
5 serializable
Spring 的传播行为
Spring 是如何管理事务的传播行为的
事务传播行为,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A事务中的方法do()调用B事务中的doOther(),事务传播行为是加在方法上的。
Spring事务的传播行为一共7种:
1 REQUIRED 指定的方法必须在事务中执行,若当前有事务则加入,没有则新建,默认的传播行为。
2 REQUIRED_NEW 总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
3 NESTED 指定的方法必须在事务内执行。若当前存在事务,则在嵌套事务内执行;若当前没有事务,则创建新事务。
4 SUPPORTS 指定的方法支持当前事务,若没有,以非事务的方式执行
5 MANDATORY 指定的方法必须在当前事务内执行,若当前没有事务,则直接抛出异常。
6 NOT_SUPPORTED 指定的方法不能再事务环境中执行,若当前存在事务,就将当前事务挂起。
7 NEVER 指定的方法不能在事务环境下执行,如果存在事务,就抛出异常。
事务传播场景
REQUIRED 将操作合并在一个事务内,任何一部分出现问题就一起回滚
REQUIRED_NEW 完全是两个事务,内外事务互不干扰 。
NESTED 内部事务是外部事务的一部分,内部事务发送回滚,则只回滚内部事务的部分。如果外部事务发生回滚,会将内外部事务一起回滚。
Spring 中体现的设计模式
工厂设计模式:Spring 使用工厂模式通过 BeanFactory ApplicationContext 创建 bean 对象
代理设计模式:Spring AOP 功能的实现
单例设计模式:Spring 中的 Bean 默认是单例的
模板方法模式:Spring 中的 jdbcTemplate 以 Template 结尾的对数据库操作的类,他们就使用到了模板。
观察者模式 :Spring 事件驱动模型就是观察者模式很经典的一个应用。
包装器设计模式:根据用户需求动态切换不同的数据源
适配器设计模式:Spring AOP 的增强或通知,Spring MVC 中使用到了适配器模式适配 Controller
Spring MVC 的运行原理
Spring MVC的执行过程:Spring的模型试图控制器框架围绕一个DispatcherServlet来设计的,这个Servlet会把请求分发给各个处理器,支持可配置的处理器映射,视图渲染,本地化,文件上传等。
1 用户请求发送给前端控制器DispatcherServlet
2 DispatcherServlet 转发请求给处理器映射器HandlerMapping 根据 url 查找handler
3 handlerMapping 返回处理器执行链给dispatcherServlet
4 前端控制器拿着执行链给处理器适配器
5 处理器适配器找到对应的handler进行业务处理
6 handler处理完之后返回ModelAndView 给处理器适配器
7 处理器适配器返回ModelAndView给前端控制器
8 前端控制器根据ModelAndView去请求视图解析器解析视图
9 视图解析器返回View对象给前端控制器
10 前端控制器进行渲染视图
11 前端控制器响应用户
前端控制器:C 控制各个组件之间解耦
Mapping : 根据用户请求url,加工成处理器的执行链
Handler : 业务逻辑的地方,Controller
处理器适配器:适配出具体的handler
视图解析器:将ModelAndView解析出View对象
MyBatis
为什么使用MyBatis?
基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响
SQL写在XML里,解除SQL与程序代码的耦合,便于统一管理,支持编写动态SQL语句,并可重用
与JDBC相比,减少很多代码量
很好的与各种数据库兼容,能够与Spring很好的集成
提供映射标签,支持对象与数据库的ORM字段关系映射
Mybatis 的执行流程
mybstis-config.xml或者mapper.xml通过configuration 加载配置文件,进行解析
通过SqlSessionFactoryBuilder创建SqlSessionFactory对象
通过SqlSessionFactory创建SqlSession对象
通过SqlSession拿到MapperProxy,通过执行器进行数据库指令下达。
Mapper 接口调用
XML映射文件会有一个Dao接口与之对应
Mapper接口的工作原理是JDK动态代理
Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy
代理对象会拦截接口方法,转而执行MapperStatement所代表的sql,然后将sql执行结果返回。
使用Mapper需要注意:
Mapper接口方法名和Mapper.xml中定义的每个sql的id相同。
接口方法的入参和xml中定义的每个sql的parameterType类型相同;
mapper接口方法的输出参数类型和xml中定义的sql的resultType类型相同。
xml文件的namespace和mapper接口的类路径相同
Mybatis 插件运行原理,如何编写插件
Mybatis可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件
Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能。
每当执行4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法。
编写插件:实现Mybatis的interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住在配置文件中配置你编写的插件才能生效。
Mybatis 如何处理入参
可以使用$ 和 # 接受入参
#{} 是预编译处理,KaTeX parse error: Expected 'EOF', got '#' at position 21: …串替换 Mybatis 在处理#̲{}时,会将sql中的#{}替…{}时,会把${}替换成变量的值
使用#{}可以有效的防止SQL注入,提高系统的安全性。
在某些特定的场景下$会更好,在字段,表名不确定的时候。
当实体类中属性名和表字段不一致时如何映射的
通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
通过resultMap来映射字段名和实体类属性名的一一对应的关系。
Mybatis 的缓存
Mybatis有一级缓存和二级缓存
一级缓存默认开启,不能关闭。指的是SqlSession级别的缓存,最多缓存1024条SQL。
二级缓存是可以跨SqlSession的缓存,是mapper级别的缓存,需要手动开启。
Springboot 的配置文件有哪些规定
Springboot的核心配置文件是application和bootstrap配置文件。
application 配置文件主要用于Springboot项目的自动化配置。
bootStrap系统级别的一些参数配置,里面的属性不能被覆盖。
bootstrap由父ApplicationContext加载,比application优先加载
配置文件格式由yml 和 properties 两种,.yml不支持@propertySource
SpringBoot如何自动加载配置文件
SpringBoot的开启注解是 @SpringBootApplication,(@Configuration @ComponentScan, @EnableAutoConfiguration:自动注入应用程序所需的所有Bean,这依赖于Springboot在类路径中的查找,他能根据类路径下的jar包和配置,动态加载配置和注入bean)
🐅 项目中是如何实现嵌入深度模型的
一般有三种方式,第一种java中直接调用python代码,通过相应的jar包就可以,相对的还有把python代码打包成jar包的方法,但是这种方式很局限,有的第三方库不支持。第二种就是保存模型的参数,用java代码重现模型的预测算法,比如tf转成pb封装输入和输出,再到java中进行使用,这个我曾经在写Android的时候使用过,这种很麻烦,很多框架也不适合。第三种就是使用进程间通信,使用socket通信。把参数传过去,把结果传回来。这种比较好。
🐧项目中用到的设计模式
设计模式的话没有仔细考虑过,但是因为是Spring框架,Spring 中一些设计模式是有用到的。例如简单工厂,BeanFactory根据传入的参数动态决定创建类,单例模式:Spring中的Bean默认是单例的提供一个全局访问点。原型模式:Bean设定为prototype。代理模式:AOP的实现就是代理模式。模板方法模式:RedisTemplate。
🐻商城项目的介绍
商城项目是源于我本科的一个毕设项目,是传统的CRUD项目,最近为了丰富相关功能增加了一个秒杀模块。秒杀系统一般都是短时间高并发,所以设计的时候,将请求拦截在系统上游,降低下游的压力。主要是通过接口隐藏,用户限流,乐观锁解决超卖实现的。项目初期主要是为了学习Redis,所以在项目中大量使用了Redis缓解数据库的压力,包括用户信息缓存,商品信息缓存,商品库存缓存,订单缓存。然后现在看来,这个问题实现的太简单了。因为秒杀项目不可能部署在一个服务器上,应该是使用分布式部署在多台服务器上,这个时候单点登录,分布式Session 就有必要。而且没有采用消息队列导致在执行数据库事务的时候可能仍然有大量请求达到数据库上。
分布式Session实现的方式
- 一台机器上的Session数据进行广播复制到集群中其余机器,适用于机器数量少,有网络延迟的开销。
- 使用粘性Session,强制指定后续所有请求均落到此机器上。这种情况下容易出现单点故障。
- 使用缓存集中管理,将Session存入分布式缓存集群中的某台机器上,用户访问不同节点时先从缓存中拿Session信息。
在这个项目中,我感觉使用第三种是比较好的,在用户登录成功之后,给这个用户生成一个sessionId,并写到token中传递给客户端,在客户端随后的访问中,都携带这个sessionId,服务端拿到这个token之后根据token来取得对应的session信息。
在项目中大量使用缓存,如何识别不同模块中的缓存(Key值重复,如何辨别是不同模块的key)
使用一个抽象类,定义BaseKey,在里面定义缓存Key的前缀以及缓存的过期时间从而实现将缓存的key进行封装,让不同的模块继承它,这样每次存入一个模块的缓存的时候,就加上这个缓存特定的前缀以及统一制定不同的过期时间,
在项目中大量使用缓存,对缓存服务器有很大的压力,如何减少redis的访问
在redis预减库存,在内存中维护一个localOverMap作为内存标记,当没有库存的时候,将其设置为true,每次秒杀业务访问redis之前,先查一下map标记,如果为true说明没有库存,直接返回秒杀失败,无需再请求redis服务器。
项目中为什么要前后端分离。
前后端分离是一个大的趋势,有很多团队合作上的优势。但是项目中我使用的主要目的是进行页面静态化,加快页面的加载速度,将商品的详情和订单详情页面做成静态HTML,数据的加载只需要通过ajax来请求服务器,并且做了静态化的HTML页面可以缓存在客户端的浏览器。
有没有使用到消息队列
消息队列目前没有使用到,但是我现在正在学习。在我这个系统中,我可以使用消息队列完成异步下单,提升用户体验,进行削峰和限流。
- 在系统初始化的时候,把商品库存数量加载到Redis中去。
- 后端收到秒杀请求,先进行Redis预减库存,如果库存到了临界值,就不需要继续请求了,直接返回秒杀失败,后面的大量请求无需给系统带来压力。
- 形成秒杀订单,将秒杀请求封装后,放入消息队列,同时给前端返回一个信息,表示正在排队。
- 后端RabbitMQ监听miaosha_queue这个名字的通道,如果有消息过来,就获取传入的信息,执行真正的秒杀事务。
- 前端根据商品id轮询请求接口result,查看是否生成了商品订单。
保证用户不能重复下单
redis 中存放一个计数器,key为商品和用户id,每次请求进行判断是不是下单成功。
但是现在想的话,在订单表中建立一个(商品id和用户id的)唯一索引或许更好,使得同一记录只能入库一次。