后端开发知识框架汇总
后端开发知识框架汇总Spring框架Spring/Springboot/SpringMVCSpring其是一个引擎,众多衍生产品例如boot、security、jpa等等;但他们的基础都是Spring的ioc和 aop,ioc 提供了依赖注入的容器, aop解决了面向切面编程,然后在此两者的基础上实现了其他延伸产品的高级功能。Springboot其是进一步实现了auto-configurati
后端开发知识框架汇总
Spring框架
Spring/Springboot/SpringMVC
Spring
其是一个引擎,众多衍生产品例如boot、security、jpa等等;但他们的基础都是Spring的ioc和 aop,ioc 提供了依赖注入的容器, aop解决了面向切面编程,然后在此两者的基础上实现了其他延伸产品的高级功能。
Springboot
其是进一步实现了auto-configuration自动配置(另外三大神器actuator监控,cli命令行接口,starter依赖),降低了项目搭建的复杂度。它主要是为了解决使用Spring框架需要进行大量的配置太麻烦的问题。
SpringMVC
其是spring基础之上的一个MVC框架,主要处理web开发的路径映射和视图渲染,属于spring框架中WEB层开发的一部分,其主要分为Model(模型)、VIew(视图)、Controller(控制器)三部分,具体的工作流程如下。
Spring的作用域
1、singleton
保持容器中只存在唯一的单例。Controller亦采用的是单例模式,同时采用ThreadLocal的方式来解决可能存在的线程不安全的问题。
2、propetype
为每个bean请求申请一个实例。申请之后不会再进行管理。
3、request
对每一个网络请求,会申请一个对应的bean容器。
4、session
会保证每个session中有一个对应的实例,在session失效后bean也会失效。
5、global-session
其与portlet有关,
IOC/DI(控制反转/依赖注入)
IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦,简单来说,**就是对象的创建不再有程序本身去创建,而是交给IOC容器去实现。**IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。Spring中实现依赖注入的主要方式是采用@Autowired的注释,
AOP(面向切面编程)
AOP即面向切面编程,其最主要的实现方式是采用代理实现的。代理又主要分为两种:静态代理和动态代理。
静态代理主要是令代理人和被代理人共同实现一个类,实现代码如下:
interface MyInterface{
public void dosomething();
}
class Principal implements MyInterface{
@Override
public void dosomething() {
System.out.println("嗨!我是被代理人!");
}
}
class Agent implements MyInterface{
Principal principal;
public Agent(Principal principal) {
this.principal = principal;
}
@Override
public void dosomething() {
System.out.println("我是代理人!");
principal.dosomething();
System.out.println("我代理完成了!");
}
}
public class StaticProxy {
public static void main(String[] args) {
Principal p = new Principal();
Agent agent = new Agent(p);
agent.dosomething();
}
}
JDK的动态代理和CGlib的动态代理。
- JDK动态代理:JDK的动态代理主要通过代理类和被代理类实现同一个接口实现,同时采用反射的机制,调用对应类的接口实现的AOP。
//JDK动态代理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface MyInterface{
public void dosomething();
}
class Principal implements MyInterface{
@Override
public void dosomething() {
System.out.println("嗨!我是被代理人!");
}
}
class Agent implements InvocationHandler {
Object principal;
public Agent(Object principal) {
this.principal = principal;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//甚至连方法都调用对应的反射!这样连组合排列都不用了!
System.out.println("-----------before-----------");
method.invoke(principal, args);
System.out.println("-----------after-----------");
return null;
}
}
public class StaticProxy {
public static void main(String[] args) {
Principal p = new Principal();
Agent agent = new Agent(p);
MyInterface myinterface = (MyInterface) Proxy.newProxyInstance(MyInterface.class.getClassLoader(),
new Class[] {MyInterface.class}, agent); //创建对应的被代理对象
myinterface.dosomething();
}
}
- CGlib动态代理:
底层实现与JDK的动态代理不同,其采用修改对应的ASM的字节码的方式实现。
@Transctional 注解:主要是通过反射获取bean的注解信息,利用AOP对编程式事务进行封装实现。?
Springboot自动装配原理
Springboot自动配置原理主要采用三个注解实现,其分别是:
1、EnableAutoConfiguration:该注解主要开启Springboot的自动配置功能。
(1) @AutoConfigurationPackage:利用register注册对应的容器,将指定的一个包下的所有组件导入进来。(默认是main包下的所有组件。)
(2) @Import(AutoConfigurationImportSelector.class):底层采用工厂模式从配置文件中读取对应的组件(注意并不是全部加载,会对组件采用@ConditionOnMissingBean等条件进行判断 )
2、SpringConfiguration,标示启动类是一个SpringConfiguration类,扫描并加载都容器中。
3、ComponetScan,指定对应的包扫描的路径。
Spring循环依赖
循环依赖–>循环引用—>即2个或以上bean 互相持有对方,最终形成闭环。eg:A依赖B,B依赖C,C又依赖A。
Spring循环依赖场景
①:构造器的循环依赖。【这个Spring解决不了】
StudentA有参构造是StudentB。StudentB的有参构造是StudentC,StudentC的有参构造是StudentA ,这样就产生了一个循环依赖的情况,我们都把这三个Bean交给Spring管理,并用有参构造实例化。
②【setter循环依赖】field属性的循环依赖【setter方式 单例,默认方式–>通过递归方法找出当前Bean所依赖的Bean,然后提前缓存【放入三层缓存中】。通过提前暴露 -->暴露一个exposedObject用于返回提前暴露的Bean。】Spring是先将Bean对象实例化【依赖无参构造函数】—>再设置对象属性的
原因:Spring先用构造器实例化Bean对象----->将实例化结束的对象放到一个Map中,并且Spring提供获取这个未设置属性的实例化对象的引用方法。结合我们的实例来看,,当Spring实例化了StudentA、StudentB、StudentC后,紧接着会去设置对象的属性,此时StudentA依赖StudentB,就会去Map中取出存在里面的单例StudentB对象,以此类推,不会出来循环的问题喽。
Spring 三级缓存
Spring在启动过程中,使用到了三个map,称为三级缓存。第一级缓存主要保存Bean到指定的容器中,而第二第三级的缓存主要用于解决循环依赖。
singletonFactories : 单例对象工厂的cache
earlySingletonObjects :提前暴光的单例对象的Cache 。【用于检测循环引用,与singletonFactories互斥】
singletonObjects:单例对象的cache
假设初始化A bean时,发现A bean依赖B bean,而B还没有初始化的话,需要暂停A的注入先注入B容器。那么此时new出来的A对象放哪里,直接放在容器Map里显然不合适,半残品怎么能用,所以需要提供一个可以标记创建中bean(A)的Map(即二级缓存),可以提前暴露正在创建的bean供其他bean依赖。而如果初始化A所依赖的bean B时,发现B也需要注入一个A的依赖(即发生循环依赖),则B可以从创建中的beanMap中直接获取A对象(创建中)注入A,然后完成B的初始化,返回给正在注入属性的A,最终A也完成初始化,皆大欢喜。这块的实现主要由第二级的缓存实现。
三级缓存中提到出现循环依赖才去解决,也就是说出现循环依赖时,才会执行工厂的getObject生成(获取)早期依赖,这个时候就需要给它挪个窝了,因为真正暴露的不是工厂,而是对象,所以需要使用一个二级缓存保存暴露的早期对象(earlySingletonObjects),同时移除提前暴露的工厂。
//具体解决依赖的代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
Spring启动过程
(1)创建beanFactory,加载xml配置文件。
(2)解析配置文件转化beanDefination,获取到bean的所有属性、依赖及初始化用到的各类处理器等。
(3)刷新beanFactory容器,初始化所有单例bean。
(4)注册所有的单例bean并返回可用的容器,一般为扩展的applicationContext。
Bean生存周期
1、实例化Bean
对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。 对于ApplicationContext容器,当容器启动结束后,便实例化所有的bean。 容器通过获取BeanDefinition对象中的信息进行实例化。并且这一步仅仅是简单的实例化,并未进行依赖注入。 实例化对象被包装在BeanWrapper对象中,BeanWrapper提供了设置对象属性的接口,从而避免了使用反射机制设置属性。
2、依赖注入
实例化后的对象被封装在BeanWrapper对象中,并且此时对象仍然是一个原生的状态,并没有进行依赖注入。
紧接着,Spring根据BeanDefinition中的信息进行依赖注入。并且通过BeanWrapper提供的设置属性的接口完成依赖注入。
3、 注入Aware接口
紧接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给bean。
4、BeanPostProcessor
当经过上述几个步骤后,bean对象已经被正确构造,但如果你想要对象被使用前再进行一些自定义的处理,就可以通过BeanPostProcessor接口实现。 该接口提供了两个函数:
- postProcessBeforeInitialzation( Object bean, String beanName )
当前正在初始化的bean对象会被传递进来,我们就可以对这个bean作任何处理。
这个函数会先于InitialzationBean执行,因此称为前置处理。
所有Aware接口的注入就是在这一步完成的。 - postProcessAfterInitialzation( Object bean, String beanName )
当前正在初始化的bean对象会被传递进来,我们就可以对这个bean作任何处理。
这个函数会在InitialzationBean完成后执行,因此称为后置处理。
5. InitializingBean与init-method
当BeanPostProcessor的前置处理完成后就会进入本阶段。 InitializingBean接口只有一个函数:
- afterPropertiesSet()
这一阶段也可以在bean正式构造完成前增加我们自定义的逻辑,但它与前置处理不同,由于该函数并不会把当前bean对象传进来,因此在这一步没办法处理对象本身,只能增加一些额外的逻辑。
若要使用它,我们需要让bean实现该接口,并把要增加的逻辑写在该函数中。然后Spring会在前置处理完成后检测当前bean是否实现了该接口,并执行afterPropertiesSet函数。
当然,Spring为了降低对客户代码的侵入性,给bean的配置提供了init-method属性,该属性指定了在这一阶段需要执行的函数名。Spring便会在初始化阶段执行我们设置的函数。init-method本质上仍然使用了InitializingBean接口。
6. DisposableBean和destroy-method
和init-method一样,通过给destroy-method指定函数,就可以在bean销毁前执行指定的逻辑。
mysql数据库
数据库引擎
数据库引擎主要分为Innodb和MyIsam。其主要的不同有以下几点:
1、锁粒度,Innodb采用行锁,且仅仅在索引生效的时候起作用,其余时候退化为表锁。MyIsam采用表锁。
2、事务性,Innodb支持事务性,而MyIsam不支持事务。
3、外键,Innodb支持外键,而MyIsam不支持外键。
4、聚簇索引,Innodb支持聚簇索引,即索引和数据放在一起,不需要进行二次回表搜索。而MyIsam不支持。
MyIsam结构如下:
Innodb结构如下:
Mysql语句查询过程
以SELECT name
,COUNT(name
) AS num FROM student WHERE grade < 60 GROUP BY name
HAVING num >= 2 ORDER BY num DESC,name
ASC LIMIT 0,2 为例子。
1、加载内存,一条查询的sql语句先执行的是 FROM student 负责把数据库的表文件加载到内存中去,如图1.0中所示。(mysql数据库在计算机上也是一个进程,cpu会给该进程分配一块内存空间,在计算机‘服务’中可以看到,该进程的状态)
2、数据过滤,WHERE grade < 60,对数据进行过滤,取出符合条件的记录行,生成一张临时表。
3、临时表划分,GROUP BY name
会把数据切分成若干临时表。
4、数据挑选,SELECT 的执行读取规则分为sql语句中有无GROUP BY两种情况。
(1)当没有GROUP BY时,SELECT 会根据后面的字段名称对内存中的一张临时表整列读取。
(2)当查询sql中有GROUP BY时,会对内存中的若干临时表分别执行SELECT,而且只取各临时表中的第一条记录,然后再形成新的临时表。这就决定了查询sql使用GROUP BY的场景下,SELECT后面跟的一般是参与分组的字段和聚合函数,否则查询出的数据要是情况而定。另外聚合函数中的字段可以是表中的任意字段,需要注意的是聚合函数会自动忽略空值。
5,二次过滤,HAVING num >= 2对数据再次过滤,与WHERE语句不同的是HAVING 用在GROUP BY之后,WHERE是对FROM student从数据库表文件加载到内存中的原生数据过滤,而HAVING 是对SELECT 语句执行之后的临时表中的数据过滤,所以说column AS otherName ,otherName这样的字段在WHERE后不能使用,但在HAVING 后可以使用。但HAVING的后使用的字段只能是SELECT 后的字段,SELECT后没有的字段HAVING之后不能使用。
6,数据排序,ORDER BY num DESC,name
ASC对以上的临时表按照num,name进行排序。
7,限制获取,LIMIT 0,2取排序后的前两个。
连接语句:
1、左外连接:是A和B的交集再并上A的所有数据。
2、右外连接:是A和B的交集再并上B的所有数据。
3、内连接,也就是返回两个表的交集(阴影)部分。
删除语句:
Delete :删除数据表中的行(可以删除某一行,也可以在不删除数据表的情况下删除所有行)。
Delete from table_name where col_name = XXX //删除某一行
Delete * from table_name
Drop:删除数据表或数据库,或删除数据表字段。
drop database db_name //删除数据库
drop table table_name // 删除特定表
alter table db_name drop column col_name //删除数据库表特定字段
Truncate:删除数据表中的数据(仅数据表中的数据,不删除表)。
truncate table table_name //删除表中数据
区别:删除数据的速度,一般来说: drop> truncate > delete。
- DELETE 语句每次删除一行,并在事务日志中为所删除的每行记录一项。执行 DELETE 语句后,表仍会包含空页。
- truncate和delete只删除表中的数据,drop会连表的结构一起删除。
- delete是DML语句,操作会被放到 rollback segment中,事务提交后才生效,以便进行进行回滚操作(可恢复)。truncate、drop是DLL语句,操作立即生效,原数据不放到 rollback segment中,不能回滚(不可恢复)。
- delete操作之后表或索引所占的空间不会减少,truncate操作之后表和索引所占用的空间会恢复到初始大小,drop语句将表所占用的空间全部释放。
删除表内部分数据用delete,删除表内所有数据并且保留表的结构用truncate,删除表或者库用drop 。
Explain SQL主要包含的信息如下(https://www.cnblogs.com/yhtboke/p/9467763.html):
id:select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序 .
Select_type:查询的类型,主要是用于区分普通查询、联合查询、子查询等复杂的查询
- SIMPLE:简单的select查询,查询中不包含子查询或者union
- PRIMARY:查询中包含任何复杂的子部分,最外层查询则被标记为primary
- SUBQUERY:在select 或 where列表中包含了子查询
- DERIVED:在from列表中包含的子查询被标记为derived(衍生),mysql或递归执行这些子查询,把结果放在零时表里
- UNION:若第二个select出现在union之后,则被标记为union;若union包含在from子句的子查询中,外层select将被标记为derived
- UNION RESULT:从union表获取结果的select
Table:指明查询语句是查询的哪一张表。
Type:访问类型,sql查询优化中一个很重要的指标,结果值从好到坏依次是:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL,一般来说,好的sql查询至少达到range级别,最好能达到ref。
- system:表只有一行记录(等于系统表),这是const类型的特例,平时不会出现,可以忽略不计
- const:表示通过索引一次就找到了,const用于比较primary key 或者 unique索引。因为只需匹配一行数据,所以速度很快。如果将主键置于where列表中,mysql就能将该查询转换为一个const。
- eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键 或 唯一索引扫描。
- ref:非唯一性索引扫描,返回匹配某个单独值的所有行。本质是也是一种索引访问,它返回所有匹配某个单独值的行,然而他可能会找到多个符合条件的行,所以它应该属于查找和扫描的混合体
- range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了那个索引。一般就是在where语句中出现了bettween、<、>、in等的查询。这种索引列上的范围扫描比全索引扫描要好。只需要开始于某个点,结束于另一个点,不用扫描全部索引。
- index:Full Index Scan,index与ALL区别为index类型只遍历索引树。这通常为ALL块,应为索引文件通常比数据文件小。(Index与ALL虽然都是读全表,但index是从索引中读取,而ALL是从硬盘读取)
- ALL:Full Table Scan,遍历全表以找到匹配的行。
possible_keys:查询涉及到的字段上存在索引,则该索引将被列出,但不一定被查询实际使用。
key:实际使用的索引,如果为NULL,则没有使用索引。 查询中如果使用了覆盖索引,则该索引仅出现在key列表中。
Key_len:表示索引中使用的字节数,查询中使用的索引的长度(最大可能长度),并非实际使用长度,理论上长度越短越好。key_len是根据表定义计算而得的,不是通过表内检索出的。
ref:显示索引的那一列被使用了,如果可能,是一个常量const。
row:根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数
Extra,不适合在其他字段中显示,但是十分重要的额外信息
1、Using filesort :
mysql对数据使用一个外部的索引排序,而不是按照表内的索引进行排序读取。也就是说mysql无法利用索引完成的排序操作成为“文件排序” 由于索引是先按email排序、再按address排序,所以查询时如果直接按address排序,索引就不能满足要求了,mysql内部必须再实现一次“文件排序”
2、Using temporary:
使用临时表保存中间结果,也就是说mysql在对查询结果排序时使用了临时表,常见于order by 和 group by
3、Using index:
表示相应的select操作中使用了覆盖索引(Covering Index),避免了访问表的数据行,效率高
如果同时出现Using where,表明索引被用来执行索引键值的查找
如果没用同时出现Using where,表明索引用来读取数据而非执行查找动作 覆盖索引(Covering Index):也叫索引覆盖。就是select列表中的字段,只用从索引中就能获取,不必根据索引再次读取数据文件,换句话说查询列要被所建的索引覆盖。
注意:
a、如需使用覆盖索引,select列表中的字段只取出需要的列,不要使用select *
b、如果将所有字段都建索引会导致索引文件过大,反而降低crud性能
4、Using where :
使用了where过滤
5、Using join buffer :
使用了链接缓存
6、Impossible WHERE:
where子句的值总是false,不能用来获取任何元祖
7、select tables optimized away:
在没有group by子句的情况下,基于索引优化MIN/MAX操作或者对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段在进行计算,查询执行计划生成的阶段即可完成优化
8、distinct:
优化distinct操作,在找到第一个匹配的元祖后即停止找同样值得动作
索引
索引可以极大的提高查询访问速度,但是会降低插入,删除,更新表的速度,因为在执行写操作的时候还要操作索引文件。
单列索引
一个单列索引只包含一列,其又具体分为如下三个索引。
- 普通索引:最基本的索引,其构造的基本SQL语句如下。
CREATE INDEX index_name
ON table_name (column_name)
- 唯一索引:唯一索引,要求值不重复,可以为空。
CREATE UNIQUE INDEX index_name
ON table_name (column_name)
- 主键索引:与唯一索引类似,但不允许有空值。
组合索引
一个组合索引包含多列值,其创建的SQL语句如下:
CREATE INDEX index_name
ON table_name (column_name1, column_name2)
组合索引的匹配服从“最左匹配”原则。如果你建立了组合索引(nickname,account,createdTime_Index),那么他实际包含的是3个索引 (nickname)、 (nickname,account) 、(nickname,account,created_time)。
聚簇索引/非聚簇索引:
1、聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。
2、在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找。辅助索引叶子节点存储的不再是行的物理位置,而是主键值。通过辅助索引首先找到的是主键值,再通过主键值找到数据行的数据页,再通过数据页中的Page Directory找到数据行。
索引数据结构
Mysql中采用的索引有B+树索引和Hash索引两种。
一、B+树索引,底层基于B+树实现,其是一种多叉的平衡树,之所以采用B+树,有以下几点原因:
1、B+树支持范围索引,相比较B树在范围搜索的时候速度更快,同时由于信息只保存在叶子节点,因此搜索的效率比较稳定。(但需要注意B树单次的搜索平均是优于B+树的)。
2、B+树树深较低大致为Logm(n),比起二叉树,红黑树深度要低,搜索速度更快。相较于B树,因为B树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少,指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低;
二、哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。对于hash相同的,采用链表的方式解决冲突。在Mysql中InnoDB引擎有一个特殊的功能叫做自适应哈希索引,它会在内存中基于B-Tree索引的基础上面创建一个哈希索引,这让B-Tree索引具备了一些哈希索引的优点。
**两者区别:**B+树索引查询效率稳定,且支持范围查询。而哈希索引的查询一次只能查询一行数据,且不支持范围查询,但查询速度要优于B+树索引。
索引优化
最左匹配:组合索引的匹配服从“最左匹配”原则。如果你建立了组合索引(nickname,account,createdTime_Index),那么他实际包含的是3个索引 (nickname)、 (nickname,account) 、(nickname,account,created_time)。因此在实际使用中,尽可能按照最左匹配的顺序使用索引。
回表:回表的意思是首先根据普通索引数,找出对应的主键值,再根据这个主键的值去对应的聚簇索引的树上进行搜索,并找到对应的行信息。因此,非聚簇索引查询了两次树,而聚簇索引只查询了一次。
索引覆盖:如果索引的叶子节点包含了要查询的数据,那么就不用回表查询了,也就是说这种索引包含(亦称覆盖)所有需要查询的字段的值,我们称这种索引为覆盖索引。
**索引下推:**当使用索引条件下推优化时,如果存在某些被索引的列的判断条件时,MySQL服务器将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器。
索引失效:
- 有or必全有索引;
- 复合索引未用左列字段,最左匹配原则。
- like以%开头,最左匹配原则。
- 需要类型转换;
- where中索引列有运算或函数;
- 如果mysql觉得全表扫描更快时(数据少);
- 如果索引存在空数据,则会失效。
事务及四大特性
事务:是作为一个单元的一组有序的数据库操作。如果组中的所有 操作都成功,则认为事务成功,即使只有一个操作失败,事务也不成功,并进行回滚撤销对数据库造成的影响。
1、原子性(Atomicity),mysql采用undo log实现,对没有完成的事务进行回滚。undo log 叫做回滚日志,用于记录数据被修改前的信息。他正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。
2、一致性(Consistency),一致性由其余三大属性一起形成。
3、隔离性(Isolation),锁+MVCC多版本并发控制。
4、持久性(Durability)。mysql提供了一个缓冲池buffer用以加快数据的检索,当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
Redo log
于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。innodb通过force log at commit机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file中进行持久化。这样可以通过读取redo log来避免在宕机(即红色线数据丢失)的情况下,损失一些已经提交的事物。(先执行第七步再执行红线部分)
为什么写入redo log会比将redo log buffer写入磁盘要快呢?
(1)刷脏是随机IO
,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
(2)刷脏是以数据页(Page)为单位的
,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。
Binlog:Mysql binlog是二进制日志文件,用于记录mysql的所有的数据更新或者潜在更新(比如DELETE语句执行删除而实际并没有符合条件的数据),在mysql主从复制中就是依靠的binlog。binlog有以下三种模式:
- row模式:日志中会记录每一行数据被修改的形式,然后在 slave 端再对相同的数据进行修改。row 模式只记录要修改的数据,只有 value,不会有 sql 多表关联的情况。
- statement模式:每一条修改数据的 sql 都会记录到 master 的 binlog 中,slave 在复制的时候,sql 进程会解析成和原来在 master 端执行时的相同的 sql 再执行。(优势是:只记录进行修改的SQL语句,而不是记录修改后对应的行,因此保存的数据量比row少。)
- mixed模式:Mixed,实际上就是前两种模式的结合。在 Mixed 模式下,MySQL 会根据执行的每一条具体的 SQL 语句来区分对待记录的日志形式,也就是在 statement 和 row 之间选择一种。
Undo log:
undo log和redo log记录物理日志不一样,它是逻辑日志。**可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。**当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。
undo log是采用段(segment)的方式来记录的,每个undo操作在记录的时候占用一个undo log segment。innodb存储引擎对undo的管理采用段的方式。rollback segment称为回滚段,每个回滚段中有1024个undo log segment。
隔离级别
未提交读
解决了更新丢失的问题,如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据,该隔离级别可以通过“排他写锁”,但是不排斥读线程实现。但可能出现脏读。
已提交读(Oracle默认)
如果是一个读事务(线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据,已提交读通过对数据的版本进行校验解决了脏读问题,但是已提交读存在不可重复读。
可重复读(mysql默认级别)
可重复读取是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写),这样就可以在同一个事务内两次读到的数据是一样的,因此称为是可重复读隔离级别,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务(包括了读写),这样避免了不可重复读和脏读,但是有时可能会出现幻读。可重复读的主要实现方式是采用MVCC+Next-Key-Lock。
可序列化
提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行,如果仅仅通过“行级锁”是无法实现序列化的,必须通过其他机制保证新插入的数据不会被执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也是最高的,性能很低。可序列化的主要实现方式是将所有的查询语句修改成对应的select in share mode,来使得事务串型执行。
Mysql锁相关
1、**记录锁(行锁):**记录锁是 封锁记录,记录锁也叫行锁,例如:
SELECT * FROM `test` WHERE `id`=1 FOR UPDATE;
它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。for update是一种行级锁,又叫排它锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行.如果其它用户想更新该表中的数据行,则也必须对该表施加行级锁.即使多个用户对一个表均使用了共享更新,但也不允许两个事务同时对一个表进行更新,真正对表进行更新时,是以独占方式锁表,一直到提交或复原该事务为止。行锁永远是独占方式锁。只有当出现如下之一的条件,才会释放共享更新锁:
- 1、执行提交(COMMIT)语句
- 2、退出数据库(LOG OFF)
- 3、程序停止运行
2、间隙锁(Gap Locks),是封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。但是间隙锁不是排他锁,有可能出现死锁的情况。
3、Next Key Lock(行锁+间隙锁),每个 next-key lock 是前开后闭区间。即包含了记录锁和间隙锁,即锁定一个范围,并且锁定记录本身。
1、当前读
像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
2、快照读
像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
Mysql死锁情况
在数据库中有两种基本的锁类型:排它锁(Exclusive Locks,即X锁)和共享锁(Share Locks,即S锁)。当数据对象被加上排它锁时,其他的事务不能对它读取和修改。加了共享锁的数据对象可以被其他事务读取,但不能修改。数据库利用这两种基本的锁类型来对数据库的事务进行并发控制。
死锁的第一种情况(访问表顺序造成死锁),一个用户A访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了。
解决方法:这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。仔细分析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进行处理,尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。
死锁的第二种情况(共享锁升级成独占锁),用户A查询一条纪录,然后修改该条纪录;这时用户B修改该条纪录,这时用户A的事务里锁的性质由查询的共享锁企图上升到独占锁,而用户B里的独占锁由于A有共享锁存在所以必须等A释放掉共享锁,而A由于B的独占锁而无法上升的独占锁也就不可能释放共享锁,于是出现了死锁。这种死锁比较隐蔽,但在稍大点的项目中经常发生。如在某项目中,页面上的按钮点击后,没有使按钮立刻失效,使得用户会多次快速点击同一按钮,这样同一段代码对数据库同一条记录进行多次操作,很容易就出现这种死锁的情况。
解决方法:
- 对于按钮等控件,点击后使其立刻失效,不让用户重复点击,避免对同时对同一条记录操作。
- 使用乐观锁进行控制。乐观锁大多是基于数据版本(Version)记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个“version”字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。乐观锁机制避免了长事务中的数据库加锁开销(用户A和用户B操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。Hibernate 在其数据访问引擎中内置了乐观锁实现。需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户更新操作不受我们系统的控制,因此可能会造 成脏数据被更新到数据库中。
- 使用悲观锁进行控制。悲观锁大多数情况下依靠数据库的锁机制实现,如Oracle的Select … for update语句,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。如一个金融系统, 当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户账户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读 出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对成百上千个并发,这样的情况将导致灾难性的后果。所以,采用悲观锁进行控制时一定要考虑清楚。
死锁的第三种情况(行锁上升为表锁)
如果在事务中执行了一条不满足条件的update语句,则执行全表扫描,把行级锁上升为表级锁,多个这样的事务执行后,就很容易产生死锁和阻塞。类似的情况还有当表中的数据量非常庞大而索引建的过少或不合适的时候,使得经常发生全表扫描,最终应用系统会越来越慢,最终发生阻塞或死锁。
**解决方法:**SQL语句中不要使用太复杂的关联多表的查询;使用“执行计划”对SQL语句进行分析,对于有全表扫描的SQL语句,建立相应的索引进行优化。
MVCC(多版本并发控制)
MVCC(多版本并发控制),是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 它的实现原理主要是依赖记录中的 隐式字段,undo日志 ,Read View 来实现的。
一、隐式字段:
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
- TRX_ID,6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
- ROLL_PTR,7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
- ROW_ID,6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
- 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
查询一条记录的流程大致如下:
- 如果被访问版本的trx_id=creator_id,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id<min_trx_id,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 被访问版本的trx_id>=max_trx_id,表明生成该版本的事务在当前事务生成ReadView后才开启,该版本不可以被当前事务访问。如果数据不可以被访问,则会按照roll_pointer查找上一个版本的记录。
- 被访问版本的trx_id是否在m_ids列表中。
4.1 是,创建ReadView时,该版本还是活跃的,该版本不可以被访问。顺着版本链找下一个版本的数据,继续执行上面的步骤判断可见性,如果最后一个版本还不可见,意味着记录对当前事务完全不可见
4.2 否,创建ReadView时,生成该版本的事务已经被提交,该版本可以被访问
二、undo日志
undo log主要分为两种:
- insert undo log
代表事务在insert新记录时产生的undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃 - update undo log
事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
三、Read View(读视图)
Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID。主要是用来做可见性判断的,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
数据库三大范式
第一范式(1NF):必须不包含重复组的关系,即每一列都是不可拆分的原子项。
缺点:存在数据冗余、更新异常、插入异常、删除异常。因此需要进一步采用更高级的范式。
第二范式(2NF):关系模式必须满足第一范式,并且所有非主属性都完全依赖于主码,但是可以有传递依赖。注意,符合第二范式的关系模型可能还存在数据冗余、更新异常等问题。举例如**关系模型(职工号,姓名,职称,项目号,项目名称)**中,职工号->姓名,职工号->职称,而项目号->项目名称。显然依赖关系不满足第二范式,常用的解决办法是差分表格,比如拆分为职工信息表和项目信息表。
第三范式(3NF):关系模型满足第二范式,所有非主属性对任何候选关键字都不存在传递依赖。即每个属性都跟主键有直接关系而不是间接关系,像:a–>b–>c。
比如**Student表(学号,姓名,年龄,性别,所在院校,院校地址,院校电话)**这样一个表结构,就存在上述关系。 学号–> 所在院校 --> (院校地址,院校电话)。我们应该拆开来,如下:(学号,姓名,年龄,性别,所在院校)–(所在院校,院校地址,院校电话)
BC范式(BCNF):是指在满足第三范式的前提下,每一个决定因素都包含码。 根据定义我们可以得到结论,一个满足BC范式的关系模式有:
- 所有非主属性对每一个码都是完全函数依赖;
- 所有主属性对每一个不包含它的码也是完全函数依赖;
- 没有任何属性完全函数依赖于非码的任何一组属性。
第四范式(4NF):
定义: 限制关系模式的属性之间不允许有非平凡且非函数依赖的多值依赖。
理解: 显然一个关系模式是4NF,则必为BCNF。也就是说,当一个表中的非主属性互相独立时(3NF),这些非主属性不应该有多值,若有多值就违反了4NF。
第五范式(5NF):投影联合范式(PJ / NF) 。 它旨在通过以多种格式分隔语义连接的关系来存储多值事实,以最大程度地减少关系数据库中的冗余。
设计模式
单例模式(懒汉型线程安全)
//懒汉单例模式,线程安全,但是性能很低。
public class SingletonModel {
private static SingletonModel single ;
private SingletonModel(){ } //虚化从而使得
public synchronized static SingletonModel GetSingletonModel() {
if (single == null){
single = new SingletonModel();
}
return single;
}
}
//懒汉单例模式,双重校验模式DCL。 保持高并发且是Lazy Loading。
public class SingletonModel {
private static SingletonModel single ;
private SingletonModel(){ } //虚化从而使得
public static SingletonModel GetSingletonModel() {
if (single == null){
synchronized (SingletonModel.class){
if (single == null){
single = new SingletonModel();
}
}
}
return single;
}
}
单例模式(饿汉型线程安全)
//饿汉直接是线程安全的。
public class SingletonModel {
private static SingletonModel single = new SingletonModel();
private SingletonModel(){ } //虚化从而使得
public static SingletonModel GetSingletonModel() {
return single;
}
}
工厂模式:
//工厂模式代码,主要的思想是采用接口来接受对应的对象。
interface Shape{
public void draw();
}
class Rectangle implements Shape{
@Override
public void draw() {
//实现对应的画形状接口
System.out.println("画矩形");
}
}
class Cycle implements Shape{
@Override
public void draw() {
//画圆形
System.out.println("画圆形");
}
}
class Square implements Shape{
@Override
public void draw() {
//画正方形
System.out.println("画正方形");
}
}
class ShapeFactory{
public Shape getShape(String str){
switch (str) {
case "Cycle":
return new Cycle();
case "Square":
return new Square();
case "Rectangle":
return new Rectangle();
}
return null;
}
}
public class Factory{
public static void main(String[] args) {
ShapeFactory sf = new ShapeFactory();
Shape s = sf.getShape("Cycle");
Shape s1 = sf.getShape("Rectangle");
Shape s2 = sf.getShape("Square");
s.draw();
s1.draw();
s2.draw();
}
}
抽象工厂模式:
观察者模式:?
//观察者模式代码
策略模式:
里氏替换原则
定义:子类对象(object ofsubtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
子类在设计的时候,要遵守父类的行为约定。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。
多线程实例
消费者生产者模型
具体代码如下:(需要学会秒写)
//生产者消费者模型,编写思路1、首先需要写对应的资源互斥区storage;2、紧接着需要写资源互斥区中get和set方法。
//3、紧接着再写Producer线程和Customer线程即可.
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
class storage{
//资源互斥区域
Deque<Integer> items= new LinkedList<>();
int size= 10;
int i=0;
public synchronized int get(){
while(items.size()<=0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
notifyAll();
return items.pollFirst();
}
public synchronized int set(){
while (items.size()>=size){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
items.addLast(i++);
notifyAll();
return i-1;
}
}
class producer extends Thread{
storage s;
producer(storage s) {
this.s = s;
}
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" puts "+ s.set());
}
}
}
class customer extends Thread{
storage s;
customer(storage s){
this.s = s;
}
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" gets "+s.get());
//睡一段时间防止多次读取
}
}
}
public class ProducerAndCustomer {
public static void main(String[] args) {
storage s = new storage();
customer c= new customer(s);
producer p = new producer(s);
customer c1 = new customer(s);
producer p1 = new producer(s);
c.start();
c1.start();
p.start();
p1.start();
}
}
线程交替打印
import java.util.concurrent.*;
public class ThreadABC {
public static class ThreadPrinter implements Runnable {
private String name;
private Object prev;
private Object self;
ThreadPrinter(String name,Object prev,Object self){
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count=10;
while(count>0){
synchronized (prev){
synchronized (self){
System.out.print(name);
count--;
self.notifyAll();
}
if(count==0){
prev.notifyAll();
}else{
try {
if (count == 0) {// 如果count==0,表示这是最后一次打印操作,通过notifyAll操作释放对象锁。
prev.notifyAll();
} else {
prev.wait(); // 立即释放 prev锁,当前线程休眠,等待唤醒
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
Object a = new Object();
Object b = new Object();
Object c = new Object();
Thread pa = new Thread(new ThreadPrinter("A", c, a));
Thread pb = new Thread(new ThreadPrinter("B", a, b));
Thread pc = new Thread(new ThreadPrinter("C", b, c));
pa.start();
Thread.sleep(10);
pb.start();
Thread.sleep(10);
pc.start();
Thread.sleep(10);
}
}
线程交替打印变形(一)
线程一打印1,2,3,4…
线程二打印A,B,C,D…
要求最后的输出结果为:1A2B3C4D…
import java.util.Scanner;
class storage{
boolean produce = true;
public synchronized void producenums(int i){
while(!produce){
try {
wait();
}
catch(Exception ignored){
}
}
System.out.println(i);
produce = false;
notifyAll();
}
public synchronized void produceletters(char c){
while(produce){
try {
wait();
}
catch(Exception ignored){
}
}
System.out.println(c);
produce = true;
notifyAll();
}
}
class Threadnums extends Thread{
storage s;
Threadnums(storage s){
this.s = s;
}
@Override
public void run(){
for(int i=0;i<100;i++){
s.producenums(i+1);
}
}
}
class Threadletters extends Thread{
storage s;
char a = 'A';
Threadletters(storage s){
this.s = s;
}
@Override
public void run(){
for(int i=0;i<100;i++){
s.produceletters((char) (a+i%26));
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
storage s = new storage();
Threadnums n = new Threadnums(s);
Threadletters l = new Threadletters(s);
n.start();
Thread.sleep(3);
l.start();
}
}
Linux
常见命令:
-wc :统计指定文件中的字节数、字数、行数,并将统计结果显示输出。
-ps:用于报告当前系统的进程状态。使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等,总之大部分信息都是可以通过执行该命令得到的。命令参数的选项如下:
- a:显示现行终端机下的所有程序,包括其他用户的程序。
- u:以用户为主的格式来显示系统状况。
- x:显示所有程序,包括历史进程。
- -e:显示所有进程(同a)
- -f:显示UID、PPIP、C与STIME栏
- -l:显示进程详细信息
-lsof:用于查看你进程开打的文件,打开文件的进程,进程打开的端口(TCP、UDP)。找回/恢复删除的文件。是十分方便的系统监视工具,因为 lsof 需要访问核心内存和各种文件,所以需要root用户执行。
git
常见命令:
-git pull:命令用于从远程获取代码并合并本地的版本。
-git fetch :命令用于从远程获取代码库。但是不与当前的代码进行合并。
-git push:命令用于从将本地的分支版本上传到远程并合并。
-git add . :他会监控工作区的状态树,使用它会把工作时的所有变化提交到暂存区,包括文件内容修改(modified)以及新文件(new),但不包括被删除的文件。
Git 与 SVN 区别
- 1、Git 是分布式的,SVN 不是:这是 Git 和其它非分布式的版本控制系统,例如 SVN,CVS 等,最核心的区别。
- **2、Git 把内容按元数据方式存储,而 SVN 是按文件:**所有的资源控制系统都是把文件的元信息隐藏在一个类似 .svn、.cvs 等的文件夹里。
- **3、Git 分支和 SVN 的分支不同:**分支在 SVN 中一点都不特别,其实它就是版本库中的另外一个目录。
- **4、Git 没有一个全局的版本号,而 SVN 有:**目前为止这是跟 SVN 相比 Git 缺少的最大的一个特征。
- **5、Git 的内容完整性要优于 SVN:**Git 的内容存储使用的是 SHA-1 哈希算法。这能确保代码内容的完整性,确保在遇到磁盘故障和网络问题时降低对版本库的破坏。
Mybatis框架
Mybatis原理
1、mybatis应用程序通过SqlSessionFactoryBuilder从mybatis-config.xml配置文件(也可以用Java文件配置的方式,需要添加@Configuration)来构建SqlSessionFactory(SqlSessionFactory是线程安全的);
2、然后,SqlSessionFactory的实例直接开启一个SqlSession,再通过SqlSession实例获得Mapper对象并运行Mapper映射的SQL语句,完成对数据库的CRUD和事务提交,之后关闭SqlSession。
详细流程如下:
1、加载mybatis全局配置文件(mybatis-config.xml),解析配置文件,MyBatis基于XML配置文件生成Configuration,和一个个MappedStatement(包括了参数映射配置、动态SQL语句、结果映射配置),其对应着<select | update | delete | insert>标签项。
2、SqlSessionFactoryBuilder通过Configuration对象生成SqlSessionFactory,用来开启SqlSession。
3、SqlSession对象完成和数据库的交互:
a、用户程序调用mybatis接口层api(即Mapper接口中的方法)
b、SqlSession通过调用api的Statement ID找到对应的MappedStatement对象
c、通过Executor(负责动态SQL的生成和查询缓存的维护)将MappedStatement对象进行解析,sql参数转化、动态sql拼接,生成jdbc Statement对象
d、JDBC执行sql。
e、借助MappedStatement中的结果映射关系,将返回结果转化成HashMap、JavaBean等存储结构ResultSet并返回。
mybatis-config.xml
其中主要包含driver(数据库驱动)、url(数据库地址)、username(连接用户名)、password(用户密码)
查询分页
例如在数据库的某个表里有1000条数据,我们每次只显示100条数据,在第1页显示第0到第99条,在第2页显示第100到199条,依次类推,这就是分页。mybatis的分页主要依靠PageInterceptor插件实现,插件的原理主要基于拦截器实现,实现方法有三种,如下:
1、采用mysql自带的limit语句(物理分页),可以对数据进行分页,但是比较麻烦。
<select id="queryStudentsBySql" parameterType="map" resultMap="studentmapper">
select * from student limit #{currIndex} , #{pageSize}
</select>
2、采用自定义插件拦截语句并修改(物理分页),即分页拦截器(paginationInterceptor)的方式,分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10
3、RowBounds分页(逻辑分页),Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
Redis框架
Redis底层原理:?
Redis集群数据一致性:?
缓存数据一致性
https://developer.aliyun.com/article/712285
首先明确只有在执行写命令的时候会出现缓存和数据库不一致的情况,那么首先第一个问题是:
更新cache还是淘汰cache?
-
淘汰cache:
优点:操作简单,无论更新操作是否复杂,直接将缓存中的旧值淘汰
缺点:淘汰cache后,下一次查询无法在cache中查到,会有一次cache miss,这时需要重新读取数据库 -
更新cache:
更新chache的意思就是将更新操作也放到缓冲中执行,并不是数据库中的值更新后再将最新值传到缓存
优点:命中率高,直接更新缓存,不会有cache miss的情况
缺点:更新cache消耗较大当更新操作简单,如只是将这个值直接修改为某个值时,更新cache与淘汰cache的消耗差不多但当更新操作的逻辑较复杂时,需要涉及到其它数据,如用户购买商品付款时,需要考虑打折等因素,这样需要缓存与数据库进行多次交互,将打折等信息传入缓存,再与缓存中的其它值进行计算才能得到最终结果,此时更新cache的消耗要大于直接淘汰cache。 且更新cache也可能带来数据不安全的情况,因此更推荐使用淘汰缓存。如果之后需要再次读取这个数据,最多会有一次缓存失败
根据先更新数据库还是淘汰缓存可以分为两种方案:
1、先淘汰缓存,再更新数据库
如果第一步淘汰缓存成功,第二步更新数据库失败,此时再次查询缓存,最多会有一次cache miss。如果更新数据库失败其实也是一个问题,为了确保数据库中的数据被正常更新,需要“重试机制”,即当数据库中的数据更新失败后,也需要人工或业务代码再次重试,直到更新成功。进一步的,在并发情况下可能导致数据的不一致。
- 采用同步更新缓存的策略,该方案可能会导致cache与数据库的数据一直或很长时间不一致。因此需要采用串行化或者延时双删+设置缓存的超时时间的方式。延时双删:A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新,B线程进行读操作,从数据库中读入旧数据,共耗时N秒。在B线程将旧数据读入缓存后,A线程将数据更新完成,此时数据不一致。A线程将数据库更新完成后,休眠M秒(M比N稍大即可),然后再次淘汰缓存,此时缓存中即使有旧数据也会被淘汰,此时可以保证数据的一致性。其它线程进行读操作时,缓存中无数据,从数据库中读取的是更新后的新数据
- 采用异步更新缓存的策略,线程只是从数据库中读取想要的数据,并不将这个数据放入缓存中,所以并不会导致缓存与数据库的不一致。在线程更新数据库后,通过订阅binlog来异步更新缓存,数据库与缓存的内容将一直一致。保证了数据的一致性,适用于对一致性要求高的业务
2、先更新数据库,再淘汰缓存
如果第一步更新数据库成功,第二部淘汰缓存失败,则会出现数据库中是新数据,缓存中是旧数据,即数据不一致。解决办法:为确保缓存删除成功,需要用到“重试机制”,即当删除缓存失效后,返回一个错误,由业务代码再次重试,直到缓存被删除。该方案即使在高并发下,也不存在长时间的数据缓存不一致的情况。但以防万一也会采用延时双删和重试机制。保证了数据读取的效率,如果业务对一致性要求不是很高,这种方案最合适
Redis事务
redis一个事务从开始到结束通常会经历以下三个阶段:
1.事务开始:MULTI,MULTI 命令的执行标志着事务的开始:
2.命令入队:QUEUED状态,当一个客户端处于非事务状态时,这个客户端发送的命令会立即被服务器执行,但是当一个客户端切换到事务状态之后,服务器会根据这个客户端发来的不同命令执行不同的操作:(如图 1-2-1)
如果客户端发送的命令是 EXEC、DISCARD、WATCH、MULTI 四个命令的其中一个,那么服务器立即执这个命令;
如果客户端发送的命令是 EXEC、DISCARD、WATCH、MULTI 四个命令以外的其他命令,那么服务器并不立即执行这个命令,而是将这个命令放入一个事务队列里,然后向客户端返回 QUEUED 回复。
3.事务执行:EXEC,
redis的事务如果执行失败是不需要回滚的,原因有以下两点:
- 只有当发生语法错误(这个问题在命令队列时无法检测到)了,Redis命令才会执行失败, 或对keys赋予了一个类型错误的数据:这意味着这些都是程序性错误,这类错误在开发的过程中就能够发现并解决掉,几乎不会出现在生产环境。
- 由于不需要回滚,这使得Redis内部更加简单,而且运行速度更快。
Redis如何实现分布式锁:
1、加锁
加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。
SET lock_key random_value NX PX 5000
值得注意的是:
random_value
是客户端生成的唯一的字符串。
NX
代表只在键不存在时,才对键进行设置操作。
PX 5000
设置键的过期时间为5000毫秒。
这样,如果上面的命令执行成功,则证明客户端获取到了锁。
2、解锁
解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value
的作用就体现出来。
为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。
public boolean unlock(String id){
Jedis jedis = jedisPool.getResource();
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
try {
Object result = jedis.eval(script, Collections.singletonList(lock_key),
Collections.singletonList(id));
if("1".equals(result.toString())){
return true;
}
return false;
}finally {
jedis.close();
}
}
上述的方法比较简单,但是问题也比较大,最重要的一点是,锁不具有可重入性。
redisson
redisson相比较上述的算法,功能更强大,其可以提供可重入锁的操作。主要的操作逻辑如下:
解锁的主要流程如下:
Redis数据结构:
我们平时主要是通过操作对象的api来操作redis,而不是通过它的调用它底层数据结构来完成(外观模式)。但我们还需要了解其底层,只有这样才能写最优化高效的代码。五种基本数据类型如下:
1、List
- list类型的value对象内部以linkedlist或ziplist承载。当list的元素个数和单个元素的长度较小时,redis会采用ziplist实现以减少内存占用,否则采用linkedlist结构
- linkedlist内部实现是双向链表。在list中定义了头尾元素指针和列表的长度,是的pop/push操作、llen操作的复杂度为O(1)。由于是链表,lindex类的操作复杂度仍然是O(N)
- ziplist的内部结构
- 所有内容被放置在连续的内存中。其中zlbytes表示ziplist的总长度,zltail指向最末元素,zllen表示元素个数,entry表示元素自身内容,zlend作为ziplist定界符
- rpush、rpop、llen,复杂度为O(1);lpush/pop操作由于涉及全列表元素的移动,复杂度为O(N)
2、String
最大可以存放512MB大小的字符串。字符串编码有三个:int、raw、embstr。
- 能表达3中类型:字符串、整数和浮点数。根据场景相互间自动转型,并且根据需要选取底层的承载方式
- value内部以int、sds作为结构存储。int存放整型数据,sds存放字节/字符串和浮点型数据
- sds内部结构:
- 用buf数组存储字符串的内容,但数组的长度会大于所存储内容的长度。会有一格专门存放”\0”(C标准库)作为结尾,还有预留多几个空的(即free区域),当append字符串的长度小于free区域,则sds不会重新申请内存,直接使用free区域
- 扩容:当对字符串的操作完成后预期的串长度小于1M时,扩容后的buf数组大小=预期长度*2+1;若大于1M,则buf总是会预留出1M的free空间
- value对象通常具有两个内存部分:redisObject部分和redisObject的ptr指向的sds部分。创建value对象时,通常需要为redisObject和sds申请两次内存。单对于短小的字符串,可以把两者连续存放,所以可以一次性把两者的内存一起申请了。
//字符串的底层源码
struct sdshdr {
// 用于记录buf数组中使用的字节的数目和SDS存储的字符串的长度相等
int len;
// 用于记录buf数组中没有使用的字节的数目
int free;
// 字节数组,用于储存字符串
char buf[];
};
//优点1、常数复杂度获取字符串长度
//优点2、杜绝缓冲区溢出
//优点3、减少了内存重新分配的次数
3、Hash
hash结构不能为每个key单独设置过期时间,主要编码方式是ziplist和hashtable。
- Hash内部的key和value不能再嵌套hash了,只能是string类型:整形、浮点型和字符串
- Hash主要由hashtable和ziplist两种承载方式实现,对于数据量较小的hash,采用ziplist实现。
- hashtable内部结构
- 主要分为三层,自底向上分别是dictEntry、dictht、dict
- dictEntry:管理一个key-value对,同时保留同一个桶中相邻元素的指针,一次维护哈希桶的内部连
- dictht:维护哈希表的所有桶链
- dict:当dictht需要扩容/缩容时,用于管理dictht的迁移
- 哈希表的核心结构是dictht,它的table字段维护着hash桶,它是一个数组,每个元素指向桶的第一个元素(dictEntry)
- set值的流程:先通过MurmurHash算法求出key的hash值,再对桶的个数取模,得到key对应的桶,再进入桶中,遍历全部entry,判定是否已有相同的key,如果没有,则将新key对应的键值对插入到桶头,并且更新dictht的used数量,used表示hash表中已经存了多少元素。由于每次插入都要遍历hash桶中的全部entry,所以当桶中entry很多时,性能会线性下降
- 扩容:通过负载因子判定是否需要增加桶数。有两个阈值,小于1一定不扩容;大于5一定扩容。扩容时新的桶数目是现有桶的2n倍
- 缩容:负载因子的阈值是0.1
- 扩/缩容通过新建哈希表的方式实现。即扩容时,会并存两个哈希表,一个是源表,一个是目标表。通过将源表的桶逐步迁移到目标表,以数据迁移的方式实现扩容,迁移完成后目标表覆盖源表。迁移过程中,首先访问源表,如果发现key对应的源表桶已完成迁移,则重新访问目标表,否则在源表中操作。
- redis是单线程处理请求,迁移和访问的请求在相同线程内进行,所以不会存在并发性问题
- ziplist内部结构
- 和list的ziplist实现类似。不同的是,map对应的ziplist的entry个数总是2的整数倍,奇数存放key,偶数存放value
- ziplist实现下,由哈希遍历变成了链表的顺序遍历,复杂度变成O(N)。
4、Set
编码的方式主要有intset和hashtable。
- hashtable中的value永远为null,当set中只包含整数型的元素时,则采用intset
- intset的内部结构
- 核心元素是一个字节数组,从小到大有序存放着set的元素
- 由于元素有序排列,所以set的获取操作采用二分查找方式实现,复杂度O(log(N))。进行插入时,首先通过二分查找得到本次插入的位置,再对元素进行扩容,再将预计插入位置之后的所有元素向右移动一个位置,最后插入元素,插入复杂度为O(N)。删除类似。
5、Zset
底层采用ziplist(跳表)和skiplist,分层提取多个节点保存,从而形成一颗类似树的结构,使得检索的速度达到O(logN)。
- 类似map是一个key-value对,但是有序的。value是一个浮点数,称为score,内部是按照score从小到大排序
- 内部结构以ziplist或skiplist+hashtable来实现。
跳表:跳表全称为跳跃列表,它允许快速查询,插入和删除一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是O(logn)。大致的结构如下:
跳表的插入:
具体到代码层面的话,每个node节点会保存一个node类型的forwards数组,大小为MAX_LEVEL,用于保存在某一层中的后继节点。
public class Node {
private int data = -1;
private Node forwards[] = new Node[MAX_LEVEL];
private int maxLevel = 0;
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{ data: ");
builder.append(data);
builder.append("; levels: ");
builder.append(maxLevel);
builder.append(" }");
return builder.toString();
}
}
Redis为什么这么快:
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门采用C语言进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗,(依据是因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽,因此可以采用单线程的方式。);
4、使用多路I/O复用模型,非阻塞IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
数据失效
1、缓存击穿:
指的是某一时刻一个热点数据失效,对应的查询数据涌到了数据库中,造成的数据库压力过大
解决方案:
(1)分布式互斥锁,只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。set(key,value,timeout) ,缺点是如果在查询数据库 + 和 重建缓存(key失效后进行了大量的计算)时间过长,也可能会存在死锁和线程池阻塞的风险。
(2)永不过期,这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况。
2、缓存雪崩
指的是大多数的热点数据在同一时间段都失效,造成大量的查询涌到了数据库中。
解决方案:
(1)缓存的过期时间用随机值,尽量让不同的key的过期时间不同;
(2)采用多级缓存,采用缓存进行兜底。本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底
3、缓存穿透
指查询一个缓存和数据库中都没有的数据,缓存穿透将导致不存在的数据每次请求都要到持久层去查询,失去了缓存保护后端持久的意义
解决方案:
(1)布隆过滤器,在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在再进入缓存层、存储层。布隆过滤器的原理,对插入的对象进行hash操作,将hash值对应的数组元素下标置为1,下次判断的时候如果布隆过滤器有对应的这多个值为1,则证明可能存在该元素,否则一定不存在这个元素。
(2)缓存空对象:是指在持久层没有命中的情况下,对key进行set (key,null)
缓存空对象会有两个问题:第一,value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
数据持久化:
Redis的持久化时间默认为
持久化的方法主要分为两种:RDB持久化和AOF持久化保存。
一、RDB持久化
其是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。对于RDB来说,提供了三种保存数据的机制:save、bgsave、自动化。
1、save触发方式,该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。具体流程如下:
执行完成时候如果存在老的RDB文件,就把新的替代掉旧的。这种方式在大数据量下显然不可取。
2、bgsave触发方式,执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体流程如下:
具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。
3、自动触发,自动触发是由我们的配置文件来完成的。
二、AOF持久化
全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。AOF的方式也同时带来了另一个问题,持久化文件会变的越来越大。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。
AOF有三种触发机制
(1)每修改同步(always):同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好
(2)每秒同步(everysec):异步操作,每秒记录 如果一秒内宕机,有数据丢失
(3)不同(no):从不同步
过期数据删除:
其实有三种不同的删除策略:
(1):立即删除。在设置键的过期时间时,创建一个回调事件,当过期时间达到时,由时间处理器自动执行键的删除操作。
(2):惰性删除。键过期了就过期了,不管。每次从dict字典中按key取值时,先检查此key是否已经过期,如果过期了就删除它,并返回nil,如果没过期,就返回键值。
(3):定时删除。每隔一段时间,对expires字典进行检查,删除里面的过期键。
可以看到,第二种为被动删除,第一种和第三种为主动删除,且第一种实时性更高。下面对这三种删除策略进行具体分析。
立即删除
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力。
而且目前redis事件处理器对时间事件的处理方式–无序链表,查找一个key的时间复杂度为O(n),所以并不适合用来处理大量的时间事件。
惰性删除
惰性删除是指,某个键值过期后,此键值不会马上被删除,而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除。所以惰性删除的缺点很明显:浪费内存。dict字典和expires字典都要保存这个键值的信息。
举个例子,对于一些按时间点来更新的数据,比如log日志,过期后在很长的一段时间内可能都得不到访问,这样在这段时间内就要拜拜浪费这么多内存来存log。这对于性能非常依赖于内存大小的redis来说,是比较致命的。
定时删除
从上面分析来看,立即删除会短时间内占用大量cpu,惰性删除会在一段时间内浪费内存,所以定时删除是一个折中的办法。
定时删除是:每隔一段时间执行一次删除操作,并通过限制删除操作执行的时长和频率,来减少删除操作对cpu的影响。另一方面定时删除也有效的减少了因惰性删除带来的内存浪费。
操作系统
进程/线程/协程/程序/纤程/管程
程序:
计算机程序是一组计算机能识别和执行的指令,运行于计算机上,满足人们某种需求的信息化工具。
进程:
进程是一段程序的执行过程,是计算机资源分配的基本单位,进程是程序的基本执行实体。同时进程拥有自己的进程控制块,系统可以利用这个进程的控制块(PCB)来控制对应的进程。
线程:
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程和进程的比较:
1、调度,线程是独立调度的基本单位,进程是资源分配的基本单位。
2、拥有资源,进程是拥有资源的基本单位,而线程则不拥有系统的资源,但是线程可以访问其所属进程的资源。
3、系统开销,系统创建和回收进程是需要对进程进行资源回收的。因此创建和回收进程的损耗要大于创建和回收线程的损耗。
4、地址空间和其他资源,进程间各自的地址空间是独立的,线程间的地址空间是共享的。
5、通信方面,进程间通信主要通过进程同步和互斥手段,而线程间可以直接读写进程数据段(如全局变量)进行通信。
协程
是用户级别的轻量级线程,协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。协程相对独立,有自己的上下文,但是其切换由程序员控制,由当前协程切换到其他协程由当前协程来控制。
纤程:
是由操作系统实现的一种轻量化线程上的一个执行结构. 通常是多个fiber共享一个固定的线程, 然后他们通过互相主动切换到其他fiber来交出线程的执行权. 各个子任务之间的关系非常强。
管程:
管程在功能上和信号量及PV操作类似,属于一种进程同步互斥工具,但是具有与信号量及PV操作不同的属性。
进程通信/同步方式:
1、管道/命名管道;2、消息队列;3、信号量;4、内存共享。
线程通信/同步方式:
1、wait()/notify()/notifyAll();2、Synchronized关键字;3、通过Condition的awiat和signal;4、 ReentrantLock 结合 Condition;5、Volatile关键字修饰的全局变量;
内存管理
内存管理主要分为以下部分:内存的空间分配和回收、地址转换、内存空间的扩充、存储保护。
1、内存的分配主要有两种:连续分配和非连续分配
连续分配主要指为一个用户程序分配一个连续的内存空间,连续分配的方式包括单一连续分配、固定分区分配、动态分区分配。
(1)单一连续分配,只用于单用户单进程的操作系统,
(2)固定分区分配,其将内存划分为多个固定大小的分区,当有空闲分区的时候,再从后续的作业队列中选择合适大小的作业装入。
(3)可变分区分配,os根据程序需要,分给每个程序所需要的内存大小,但会出现内存碎片的问题。通常需要采用紧凑技术进行解决。可变分区的分配策略有以下几种:
- 首次适应算法,空闲分区以地址递增次序连接,找到大小能满足要求的第一个分区。
- 最佳适应算法,空闲分区按容量递增次序连接,找到第一个能满足要求的空闲分区。
- 最坏适应算法,空闲分区以容量递减次序连接,挑选第一个最大的分区。
- 邻近适应算法,与首次适应类似,只不过每次都从上一次的位置开始查找。
非连续分配算法主要有三种实现方式:分页式存储、分段式存储、段页式存储;
(1)分页式存储:主要将内存划分成大小固定的页面,在程序申请内存的时候,分配适合大小的页面。这种方式可以有效减少内存的碎片,同时不需要数据连续存放。同时内存中会维护一张页表,用于对内存地址进行转换。作业的逻辑地址:页号+页内偏移。
(2)分段式存储:主要是从用户的角度出发,按照程序的自然段划分为逻辑空间。同样内存中会维护一张段表,每个段表项对应内存中的一段。内存地址借助于段表寄存器进行转换,作业的逻辑地址为:段号+段内偏移
(3)段页式存储:结合了上述两种方法的优点,首先会对作业进行分段,分段之后再保存到对应的每个页中。内存管理仍然和分页式存储一致。作业的逻辑地址分为三部分:段号+页号+逻辑地址。
虚拟内存
虚拟内存指的是将程序划分成多个页块,将程序运行所需要的页块调入到内存中,其余的页块保存在硬盘中。如果程序访问到一些不在内存中存在的页块(产生了缺页中断),就采用页面的置换算法进行置换。(虚拟地址的格式一般为:虚拟块号+页内偏移。而物理地址一般为:物理块号+页内偏移)流程为:查询页表,将虚拟页号按照页表转换成对应的物理块号,同时将物理块号换入到内存中,并在访问一次,即完成虚拟地址到物理地址的转换。
页面置换算法:
1、OPT算法,最佳置换算法。所选择的被换出的页面将是以后永不使用的,或者是在最长时间内不再被访问,可以保证获得最低的缺页率。但是由于人们无法预知一个页面多长时间不再被访问,因此该算法是一种理论上的算法。
开始运行时,先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,根据最佳置换算法,页面 7 在第18次访问才需要调入,再次被访问的时间最长,因此会将页面 7 换出,以此类推,具体过程如下图;
2、FIFO算法,先进先出置换算法,选择换出的页面是最先进入的页面。该算法实现简单,但会将那些经常被访问的页面也被换出,从而使缺页率升高。
FIFO算法还会产生当分配的物理块数增大而页故障数不减反增的异常现象,称为Belady异常。FIFO算法可能出现Belady异常,而LRU和OPT算法永远不会。
3、LRU算法,最近最久未使用置换算法。LRU 将最近最久未使用的页面换出,它认为过去一段时间内未使用的页面,在最近的将来也不会被访问。实现方式一是在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面时最近最久未访问的。实现方式二是为每个页面设置一个访问字段,来记录页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰。
LRU性能较好,但需要寄存器和栈的硬件支持,LRU是堆栈类的算法,理论上可以证明,堆栈类算法不可能出现Belady异常,FIFO算法基于队列实现,不是堆栈类算法。
4、第二次机会算法,FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索。
3、Clock算法,时钟置换算法。最简单的时钟策略需要给每一页框关联一个附加位,称为使用位。当某一页首次装入内存中时,则将该页页框的使用位设置为1;当该页随后被访问到时(在访问产生缺页中断之后),它的使用位也会被设置为1。当一页被置换时,该指针针被设置成指向缓冲区中的下一页框。当需要置换一页时,操作系统扫描缓冲区,以查找使用位被置为0的一页框。每当遇到一个使用位为1的页框时,操作系统就将该位重新置为0;如果在这个过程开始时,缓冲区中所有页框的使用位均为0时,则选择遇到的第一个页框置换;如果所有页框的使用位均为1时,则指针针在缓冲区中完整地循环一周,把所有使用位都置为0,并且停留在最初的位置上,置换该页框中的页。当需要使用的页已经存在时,则指针不会受到影响,不会发生转动。
常驻内存
常驻内存,这个术语来自MSDOS的时代。MSDOS是单任务的运行环境,系统一般不允许两个以上程序同时运行。也就是说,如果你正在运行一个任务,而又想运行另外一个任务,你必须退出当前的任务。有一种辅助工具程序,能假装退出,而仍驻留于内存当中,让你运行其它的应用。而当你需要的时候,可以用热键随时把该驻留程序激活。这样就看起来像多任务,并用这种方式为用户提供方便。一般这样的程序都是很小的应用程序。占用内存极少。或者占用高端内存。在现代的多任务操作系统中,常驻内存程序,只不过是个名词而已,其内涵早就失去了实际意义。这不是说没有可以常驻内存的程序,而是把程序区分为常驻内存和非常驻内存,无论从技术或者使用的角度来说,都毫无意义。
写时复制
写时复制的技术,它通过允许父进程和子进程最初共享相同的页面来工作。这些共享页面标记为写时复制,这意味着如果任何一个进程写入共享页面,那么就创建共享页面的副本。
五种IO模型
BIO
其是Blocking IO的意思。在类似于网络中进行read
, write
, connect
一类的系统调用时会被卡住。举个例子,当用read
去读取网络的数据时,是无法预知对方是否已经发送数据的。因此在收到数据之前,能做的只有等待,直到对方把数据发过来,或者等到网络超时。
NIO
是指将IO模式设为“Non-Blocking”模式。在NIO模式下,调用read,如果发现没数据已经到达,就会立刻返回-1, 并且errno被设为EAGAIN
。具体来说NIO就是不断的尝试有没有数据到达,有了就处理,没有就等一小会再试。NIO的底层原理主要是依靠缓冲区来实现的,输入通过管道输入到缓冲区,之后OS系统再通知对应的线程从缓冲区中提取数据,从而实现无需等待的过程。
IO多路复用
IO多路复用(IO Multiplexing) 是这么一种机制:程序注册一组socket文件描述符给操作系统,表示“我要监视这些fd是否有IO事件发生,有了就告诉程序处理”,这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO复用模型的思路。
IO复用模型的思路就是系统提供了一种函数可以同时监控多个fd的操作,这个函数就是我们常说到的select、poll、epoll函数,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,select函数监控的fd中只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起recvfrom请求去读取数据。
信号驱动IO模型
复用IO模型解决了一个线程可以监控多个fd的问题,但是select是采用轮询的方式来监控多个fd的,通过不断的轮询fd的可读状态来知道是否就可读的数据,而无脑的轮询就显得有点暴力,因为大部分情况下的轮询都是无效的,所以有人就想,能不能不要我总是去问你是否数据准备就绪,能不能我发出请求后等你数据准备好了就通知我,所以就衍生了信号驱动IO模型。
于是信号驱动IO不是用循环请求询问的方式去监控数据就绪状态,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后再通过SIGIO信号通知线程数据准备好后的可读状态,当线程收到可读状态的信号后,此时再向内核发起recvfrom读取数据的请求,因为信号驱动IO的模型下应用线程在发出信号监控后即可返回,不会阻塞,所以这样的方式下,一个应用线程也可以同时监控多个fd。
异步IO模型
其实经过了上面两个模型的优化,我们的效率有了很大的提升,但是我们当然不会就这样满足了,有没有更好的办法,通过观察我们发现,不管是IO复用还是信号驱动,我们要读取一个数据总是要发起两阶段的请求,第一次发送select请求,询问数据状态是否准备好,第二次发送recevform请求读取数据。应用只需要向内核发送一个read 请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用,我们称这种一劳永逸的模式为异步IO模型。
这种模型与信号驱动模型的主要区别在于,信号驱动IO只是由内核通知我们合适可以开始下一个IO操作,而异步IO模型是由内核通知我们操作什么时候完成。
**系统调用:**Linux内核中设置了一组用于实现各种系统功能的子程序,称为系统调用。用户可以通过系统调用命令在自己的应用程序中调用它们。从某种角度来看,系统调用和普通的函数调用非常相似。区别仅仅在于,系统调用由操作系统核心提供,运行于核心态;而普通的函数调用由函数库或用户自己提供,运行于用户态。
系统调用大致可分为六大类:
进程控制(process control)、文件管理(file manipulation)、设备管理(device manipulation)、信息维护(information maintenance)、通信(communication)、保护(protection)
IO管理
程序访问磁盘时,CPU的控制方式主要有以下几种:
1、程序直接控制方式,CPU会对外设状态进行循环检查,这种方式需要CPU一直参与IO。
2、中断驱动方式,允许IO设备主动打断CPU的运行并执行对应的服务,从而可以解放CPU。
3、DMA方式,又称为直接存储器存放,不经过cpu,直接从IO设备读入到内存中。基本传送单位是块。
4、通道控制方式,IO通道是指专门负责输入/输出的处理机。其进一步减少CPU的干预,数据的传送不再需要经过CPU而是直接采用通道进行处理。
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,四大必要条件:
1、互斥访问;
2、不可剥夺;
3、请求并保持;
4、循环等待。死锁解除的方法对应就是破坏四大必要条件。
死锁避免:银行家算法
中断
中断处理的基本过程包括中断请求、中断判优、中断响应、中断服务 和中断返回等五个阶段。
中断请求阶段
1)发生在CPU内部的中断(内部中断),不需要中断请求,CPU内部的中断控制逻辑直接接收处理。
2)外部中断请求由中断源提出。外部中断源利用CPU的中断输入引脚 输入中断请求信号。一般CPU设有两个中断请求输入引脚:可屏蔽中断请求输入引脚和不可屏蔽中断请求输入引脚。
中断判优阶段
CPU一次只能接受一个中断源的请求,当多个中断源同时向CPU提出中断请求时,CPU必须找出中断优先级最高的中断源,这一过程称为中断判优。中断判优可以采用硬件方法,也可采用软件方法。
中断响应阶段
经过中断判优,中断处理就进入中断响应阶段。中断响应时,CPU向中断源发出中断响应信号,同时:
① 保护硬件现场;
② 关中断;
③ 保护断点;
④ 获得中断服务程序的入口地址。
中断服务阶段
中断服务程序的一般结构为:
1)保护现场。 在中断服务程序的起始部分安排若干条入栈指令,将各寄存器的内容压入堆栈保存。
2)开中断。 在中断服务程序执行期间允许级别更高的中断请求中断现 行的中断服务程序,实现中断嵌套。
3)中断服务。 完成中断源的具体要求。
4)恢复现场。 中断服务程序结束前,必须恢复主程序的中断现场。通常是将保存在堆栈中的现场信息弹出到原来的寄存器中。
5)中断返回。 返回到原程序的断点处,继续执行原程序。
中断返回阶段
返回到原程序的断点处,恢复硬件现场,继续执行原程序。
中断返回操作是中断响应操作的逆过程。
项目相关逻辑:
一、避免超卖的手段:
具体避免超卖的方式有哪些?你是怎么做的?
答:首先是库存校验,通过select语句判断当前的库存是否小于0,如果小于0则返回错误。其次,在多线程下,采用Synchronized或者是数据库中采用乐观锁的方式,来保证线程有序访问对应的库存。
//乐观锁的方式
<update id="updateSale" parameterType="com.oversold.entity.Stock" >
update stock set sale=sale+1,version=version+1 where id = #{id} and version=#{version}
</update>
进一步的,考虑到秒杀系统往往是对单个系统中的方法进行访问,那么我还考虑了令牌桶的算法(漏桶算法也可以进行限流)对接口限流,具体操作如下:
private RateLimiter rateLimiter = RateLimiter.create(40);// 创建40个令牌。
boolean canBuy = rateLimiter.tryAcquire(5,TimeUnit.SECONDS) //在线程访问的时候,首先去获取对应的令牌以进行访问。
最后,考虑到秒杀系统往往是有秒杀的时间的,于是我采用了redis进行秒杀数据的时间限制。主要方法是,通过向redis中添加对应的商品号并设置对应的过期时间,在校验库存之前,会去redis中判断当前的商品是否处于可以购买的一个状态。如果存在该商品,则继续执行下面的业务逻辑。否则就中断并报错。
if(!stringRedisTemplate.hasKey("kill" + id)){
throw new RuntimeException("当前抢购已经结束!");
}
同时,对于每个用户,其可能出现一时间段内访问多次的情况。因此还用redis对用户做了访问次数的限制。
二、如何判断当前线程是否更新数据成功呢?
在项目中,调用了对应DAO层的updateSale(stock);,其会返回对应的修改行数–updateRow,如果行数大于1则证明当前的数据库被更新。否则当前的数据库没有更新,同时返回给用户对应的信息。
三、整个业务流程是怎么样的呢?你是在哪里采用的锁的机制呢?
项目实现的基本功能有注册、登陆;查看商品的信息,查看商品的图片。同时有对应的秒杀模块,能够防止商品出现超卖。主要核心的业务在于购买,用户点击购买后会将商品加入到购物车中,在购物车中再次点击购买会进入订单核对的界面,对订单进行确认后,会去校验对应的库存,如果库存充足则库存-1同时生成对应的订单码。
如果库存不足则提示购买者商品不足同时返回错误消息。
四、如何实现持久化登陆?
项目中采用cookies的方式,用户首次登陆的时候将用户的信息保存到cookies中并发送给客户端。客户端二次登陆的时候,会将对应的cookies发送到服务器端。通过匹配对应的cookies中是否包含用户信息来判断当前是否已经登陆过。
// 将登录用户信息保存到session中
session.setAttribute(Const.SYSTEM_USER_SESSION, dbUser);
// 保存cookie,实现自动登录
Cookie cookie_username = new Cookie("cookie_username", username);
// 设置cookie的持久化时间,30天
cookie_username.setMaxAge(30 * 24 * 60 * 60);
// 设置为当前项目下都携带这个cookie
cookie_username.setPath(request.getContextPath());
// 向客户端发送cookie
response.addCookie(cookie_username);
(如果采用session的方式,则具体的代码如下:)
HttpSession session = request.getSession();
// 将用户信息从session中删除
session.removeAttribute("userInfo");
Object userInfo = session.getAttribute("userInfo");
五、保存的用户登陆信息在数据库中是明文保存的吗?如果考虑加密的话,该采用能解密的加密算法吗?
其实不需要采用可以解密的算法(如md5),因为密码可能涉及个人信息,而且不需要解密,往往只需要两次输入的密码进行加密后,比较的值一样即可。
六、那如果用户购买了商品后想退款怎么做呢?
目前没有设计,但如果需要退款的话可以设置对应的退款流程,填写退款原因—商家协调进行退款-----撤销单号-----修改库存----返还金额。
七、登陆注册部分,如何保证两次注册的用户名字是不一样的呢?
1、注册的时候进行校验;2、设置主键或唯一索引;
参考资料:
更多推荐
所有评论(0)