属于自己的"超级账本仓库",因为以前记笔记是分了很多文件去记录的,这就导致无法使用全文检索,去找到我记录的知识点,所以准备尝试,将所有笔记代码记录在一起,可以快速定位到笔记内容,从而解决问题!
本次账本记录与后端一切相关知识!
切记因为笔记可能会变得十分臃肿,所以一定要分好类!分类原则秉承着,宁愿多分,也不要少分的原则。一级标题与二级标题都属于大分类,按照分类标准来写,三级分类属于小标题,按照时间来写。三级标题格式为 内容 2019年8月23日 12:07:19这种形式
本分类包含内容:
- JDK内置api(如时间localdate api)
- java基础语法(如lambda)
首先我们需要明确,注解本身是没有任何功能的,就和xml一样。注解和xml都是元数据的一种。元数据即描述数据的数据,这就是所谓的配置。注解的功能来自用这个注解的地方。在原始的使用中,我们可以通过反射来获取当前方法标注的注解内容,然后根据注解的内容决定方法是否执行,方法执行时是否真的调用,方法执行时是否需要某些特殊处理,这一点在于原生aop编程测试时,非常明显!
通过ascii比较排出的顺序也叫做字段顺序,字典排序规则如下,在进行签名算法的时候很常见!
在java编程中,我们会偶尔遇到字符串大小比较的问题,compareTo()方法很简单就实现这种功能。该方法用于判断一个字符串是大于、等于还是小于另一个字符串。判断字符串大小的依据是根据它们在字典中的顺序决定的。
语法:Str1.compareTo(Str2);
其返回的是一个int类型值。若Str1等于参数字符串Str2字符串,则返回0;若该Str1按字典顺序小于参数字符串Str2,则返回值小于0;若Str1按字典顺序大于参数字符串Str2,则返回值大于0。
java中的compareto方法,返回参与比较的前后两个字符串的asc码的差值,看下面一组代码
String a="a",b="b";
System.out.println(a.compareto.b);
则输出-1;
若a="a",b="a"则输出0;
若a="b",b="a"则输出1;
单个字符这样比较,若字符串比较长呢??
若a="ab",b="b",则输出-1;
若a="abcdef",b="b"则输出-1;
也就是说,如果两个字符串首字母不同,则该方法返回首字母的asc码的差值;
如果首字母相同呢??
若a="ab",b="a",输出1;
若a="abcdef",b="a"输出5;
若a="abcdef",b="abc"输出3;
若a="abcdef",b="ace"输出-1;
即参与比较的两个字符串如果首字符相同,则比较下一个字符,直到有不同的为止,返回该不同的字符的asc码差值,如果两个字符串不一样长,可以参与比较的字符又完全一样,则返回两个字符串的长度差值
- 实现特定功能的思路(一般是非服务端开发的功能思路,服务端开发的思路放在JavaEE相关模块下面)
- 对一些词汇的理解
- 一句话概括某项变成事物
- 为什么会出现某种编程现象的理解(比如为什么要使用分布式,读写分离)
返回void其实并不代表不会影响方法外的内容,最典型的例子就是,方法返回void,但是通过httpservletresponse返回一个错误页面等等,再方法内对对象进行操作,当然会影响方法外的这个对象的内容,因为变量保存的是方法的指针!!
!!!就是看日志,无论发生什么错,都不要去猜,先看日志,日志看不懂了再去适当的猜!!!
日志可以使控制台直接输出的,也可以是输出到日志文件中的,比如这次在安装mongodb的时候,命令行完全不报错,但是服务安装不上,通过输出日志可以发现日志中有拒绝访问的提示,可以预想到,会发生权限不足的情况,然后通过管理员去安装,发现安装成功!!!所以看日志真的太重要了啊!!!
其实与框架是一样的,工具类的出现一定是简化了我们的工作量,我们要付出的仅仅是学习成本!就比如阿帕奇的IO工具类,虽然学习成本有那么一点点,但是学会了,写出的代码,性能好,逼格高,安全,还简单,让我们编程更加快乐!在学习的时候不要视图把所有框架的底层源码搞清楚,这是不现实的,我们要做的是专精,根据工作或自身的需要看懂一个框架就很好了,不要拿到一个东西,先想着能不能搞清楚源码,这种思路是不对的,应该想着怎么解决现在的痛点,解决痛点后如果项目需要将源码搞清楚,再进行深扒!!!
首先当我们看别人代码觉得很复杂的时候,那是因为我们能看的项目一般已经维护了很久,已经抽取了无数次,所以代码不好理解!
在我们看开源项目的时候,一定要找到一个入口,这里的入口不是项目启动的入口,而是根据http请求或接口调用的API来确定入口,然后一条路拔下去。
具体的套路为:
- 当时一个web项目时
- 先根据web页面找到发送的请求
- 在后端项目中找到接收本次请求的controller
- 通过这个controller入口,结合自身学习的大量知识,以及代码的堆栈调用一点一点的研究
- 当是一个框架时
- 通过api的调用找到某个类
- 找到的这个类就是一个入口
- 通过这个入口,以及自身学习的大量相关知识根据堆栈调用研究下去!
可以看出以上寻找入口后成功后,都需要研究堆栈调用,这种情况下,我们就需要下载源码,通过在源码寻找入口以及打断点进行堆栈调用!因为在我们通过maven导入jar包其实是编译后的文件,我们没办法通过搜索快速定位到入口!
通过以上总结,其实controller只是用来做调用的,controller会调用service层的代码,所以我们重点关注的是service层的代码!
如上图开源代码所示,controller只是调用AdminService,但是在AdminService中会调用其他service的代码,这也应该是我们写代码时需要注意的点!业务代码只能一句一句的去看,硬着头皮去调试!任何人都是这么过来的,只是有人坚持下来了,有人放弃了!
Service只会调用自己的Dao层以及,别的Service层代码,绝对不要去调用别的Dao层代码,否则复用性几乎为0。
这个问题困扰了我很久,现在就要揭晓啦!
其实我一开始想的并没有错,要理解枚举类,就要从枚举这个词出发。枚举,顾名思义就是数的清的东西。那么枚举类中我们是要数的清什么呢?对的就是枚举类的实例化对象。
枚举类与普通类最大的区别是,普通类的实例化对象时不可枚举的!而枚举类的实例化是可枚举的,讲人话的意思就是,普通类可以实例化无限多个对象,而枚举类能实例化的对象是有限的个数,别抬杠啊,除非你想去工地干活。
枚举类的对象不仅是可枚举的,并且枚举类对象实例化的代码就在枚举类中。定义一个标准的枚举类格式如下:
public eum MainEum{
AAA("张三");
private string text;
//省略getter与setter
private MainEum(string UpText){
this.text = UpText;
}
}
所有的锁其核心都是为了控制并发访问。锁最终实现的是在同一个时刻下,只有一个线程对某项资源进行访问。即将并行访问,变成一个串行化的访问模式。
无论是数据库还是程序中,锁都可以分为悲观锁和乐观锁,这是锁的两种理念。悲观锁由于在访问前会锁定资源,导致别的访问该资源的线程处于阻塞状态,所以悲观锁的性能并发性能不高。乐观锁通过结果集的版本号字段,在提交修改时才对数据进行校验,如果版本号不一直,那就说明数据不对,然后将具体的后续处理交友设计人员来设计。乐观锁由于不会对数据先进行上锁再修改,所以当前数据在并发访问的情况下,就不会阻塞别的线程访问该资源,所以并发性能更高。
在spring框架中,无论是xml配置,注解配置,都被称为配置元数据,所谓元数据即描述数据的数据。元数据本身不具备任何可执行的能力,只能通过外界代码来对这些元数据解析后进行一些有意义的操作。spring容器解析这些配置元数据进行bean的初始化、配置和管理依赖。比如生命bean的配置:@Component、@Service等。注入bean的注解:@Autowried等,都属于元数据。
首先我在看到这个注解的时候,发现他是一个组合注解,被这个注解标注的bean是否加载,本质上是由@Condition来决定的,于是理所当然的我就点击进去OnWebApplicationCondition,希望能找到matches方法,但是点击进去后发现
当前类并没有matches方法(这里已经展示了UML类图),我们需要先找到实现matches方法的地方,因为当前注解需要的类如下
@Condition注解追中需要的类型是Condition,所以该注解在运行的时候,只能通过向上转型的方式调用到(因为他也跟不知道子类有啥别的方法,当然也无法进行调用)
Condition接口类型的matches方法。回到OnWebApplicationCondition类继续向上跟,找到matches方法,因为只有这个方法最终被调用,一切的逻辑落脚掉都是这个方法,只要找到这个方法就好办了,根据子类没有就在父类中找的原则我们找到这个类
发现他就是实现了Condition接口的matches方法,我们就继续跟这个方法,先从return看
我们发现返回boolean是通过这个类的isMatch方法来实现的,那就先看看怎么获取的这个类,点击getMatchOutcome方法,发现在当前类中getMatchOutcome方法是一个抽象方法,那肯定在子类中有他的实现
这时候我们就需要跟这个方法的实现了,
因为我们现在跟的是webCondition,找到他的实现类
顺利的找到这个方法了,依旧是先看方法的return部分我们发现ConditionOutcome.match(...)是一个工厂方法,用于创建ConditionOutcome的实例对象,在该对象中我们可以发现
与
此事再回头看
就显得很清楚了,逻辑就是通过判断构造一个ConditionOutcome对象实例,这个对象实例调用isMatch()就会返回是否匹配成功的布尔值,从而实现根据条件注入当前标注举个例子,就是下图中的
WebFluxAutoConfiguration这个bean是否会被加入到ioc容器中。记住组合注解仅仅就是将几个注解的功能进行组合,而不会改变功能本身,所以ConditionalOnWebApplication并不会改变@Conditional注解的作用@Conditional如果调用matches返回false的话,这个bean就是不会被加载入IOC容器中的!
在使用maven进行工程搭建的时候,无论是eclipse还是idea其实都是一样的!
我们在搭建maven结构的项目时,一定要严格遵守maven的规范,如果不遵守maven的规范,那么项目时不可能正常运行的!所以今天我们就来说一说,问什么严格遵守maven规范,就能够正常运行一个项目。
首先想要运行maven结构项目的前提是我们在电脑上安装了maven,这样子我们才能通过安装的maven去读取我们的项目,因为maven软件在读取项目进行编译打包的时候,就会根据maven软件预设的一些文件名进行加载,如果加载错误当然就不能正常的运行!
同理我们在创建一个maven的web项目时,也需要按照maven的要求来,这就是maven软件规定的,没什么道理,就算找到为什么要叫pom.xml之类的,也是对开发没什么实质性的帮助。对于软件的使用记住就好了!
综上,创建一个基于maven结构的web项目,我们需要知道的就是严格按照maven的规范来,maven就是将webapp以及各个目录按照一定的组织形式打成一个war包,那我们就得按照maven的来,这是没什么深刻道理的。**再次强调,对于软件的使用,只要知道套路就是最低也是最高的要求了而maven显然就是一个软件!**我们对于"软件"的理解不要太狭隘,准确的说,辅助或促使我们工作的应用都可以称为一个软件,而不是点击下一步下一步安装的才叫软件啊!!!
在maven的web项目中,对的配置就是对tomcat的配置也就是我们在没有用ide的时候通过
这个文件进行的配置。
而maven结构的src/webapp/WEB-INF/web.xml就是对tomcat中每个webapp独有的那个web.xml的配置,这点我们可以重复听听方立勋的课程,会有更清晰的理解!
总体而言编码的意思就是:根据字节数组对照码表,将字节数组编成码表中对应的字符。所以编码后返回的是字符或字符串
一个字节数组包含很多字节,而一个字节有8位,所以每个字节数组都能用一个数字来表示,比如01010111。这样的话,根据对应字符集的查找规则(1个字节查找法,2个字节查找发,3个字节查找法等等),就可以在码表中查询到唯一的字符串。所以,在我们进行编码的时候,一定会将字节按照某个码表编成字符串。更深一层这也就是乱码产生的原因,比如:原本的字符串是用a码表进行字符数组的获取(通过字符串获取字符数组),但是用b码表进行编码(将字符数组编写成字符串),这样子数据肯定就乱了,甚至超越了当前码表能表示的范围,造成数据错乱。但是如果两个码表有重叠的部分,那么数据有可能幸运的就一样的,不过我们应该知道,这还是“错误”数据!
base64编码,就是通过将数据降维,将三个字节的数据放在四个字节中,缺省的位用0补全,从而保证查询base64码表的时候,数据变成键盘上可以看到的字符,从而防止产生特殊字符,导致数据传输出现问题,因为数据在传输的过程中总有一个开始符号和一个结束符号,如果不进行BASE64编码的话,万一传输的数据中出现结束符号,那就会导致数据传输提前结束,数据不完整。
图示,将3个字节通过BASE64编码规范转换为4个字节的情况
其实本质上就是平均将3个字节,3*8=24位,平均放到4*8=32位中,没有的位直接补0。从而将字符的范围缩小到0000000到00111111,之间一共是2的6次方个数字,所以BASE64码表也应该有64个符号,码表如下
从侧面说明我们的猜想是正确的!
什么是数据快照呢?就是在我们开发业务系统的时候,有这样一种场景,某项操作对应的是某一个时间点的数据,而数据本身是可变的。为了今后的归档,我们就需要将操作的数据进行快照,相当于在数据库中存有两份该数据,一份是原本可变的数据,另一份是快照时间点,不可变的数据。
首先我们需要知道,引入缓存势必会导致数据的不一致,所以我们需要在这里做一定的平衡,通过代码的设计去最大程度的减少数据不一致产生的可能性。
主要是在更新数据时,应该先清除缓存还是先更新数据库中的数据再清除缓存呢?答案是肯定的,应该优先更新数据库中的数据,再清除缓存,从而实现缓存更新的效果。这样做主要的原因就是,如果先清除缓存,如果我们在清除缓存在更新数据库时,客户端发送了一次请求,此时会将旧数据加入缓存中,导致数据库更新完毕后,缓存中还是旧的数据。
我们在进行秒杀活动的时候,需要进行预下单处理,这样子就会释放部分DB的压力,原理如下,我们首先将商品的库存等信息加载到redis中,然后在进行秒杀活动的时候,优先查询redis中的订单数量,如果数量充足那就对redis的库存进行减库存操作,然后再将请求方形到DB上,
这个HASH算法不是大学里数据结构课里那个HASH表的算法。这里的HASH算法是密码学的基础,比较常用的有MD5和SHA,最重要的两条性质,就是不可逆和无冲突。所谓不可逆,就是当你知道x的HASH值,无法求出x;所谓无冲突,就是当你知道x,无法求出一个y, 使x与y的HASH值相同。这两条性质在数学上都是不成立的。因为一个函数必然可逆,且由于HASH函数的值域有限,理论上会有无穷多个不同的原始值,它们的hash值都相同。MD5和SHA做到的,是求逆和求冲突在计算上不可能,也就是正向计算很容易,而反向计算即使穷尽人类所有的计算资源都做不到。我觉得密码学的几个算法(HASH、对称加密、公私钥)是计算机科学领域最伟大的发明之一,它授予了弱小的个人在强权面前信息的安全(而且是绝对的安全)。举个例子,只要你一直使用https与国外站点通讯,并注意对方的公钥没有被篡改,G**W可以断开你的连接,但它永远不可能知道你们的传输内容是什么。
hash算法别的,详见目录下/page/常见的hash算法及其原理 - Beyond_2016的博客 - CSDN博客
总体来说,因为hash算法是特定的映射结果,所以可能会出现倾斜性结果。而不是说hash算法一定会出现倾斜性问题。
在日常工作中,经常有这样的情况,我们需要做hash,散列开数据到不同的区或节点。目标要的结果是要均匀散列,避免某个节点积累大量的数据,出现倾斜情况。
比如目前有N台机器,过来的数据key,需要做散列key%N,分发到对应的节点上。
一致性哈希算法原理 为了解决hash倾斜难题,一致性算法是这样的,节点和节点形成一个环。比如
A->B->C->A,这样一个环。数字hash后落在环上,而不是落到某个node。比如落在a~b node之间,通过顺时针转,这个数字归b节点管。
但是如果节点很少,同样容易出现倾斜,负载不均衡问题。所以一致性哈希算法,引入了虚拟节点,在整个环上,均衡增加若干个节点。比如a1,a2,b1,b2,c1,c2,a1和a2都是属于A节点的。
通过让闭环上的节点增加,来平衡各个节点散列的值。
首先我们应该知道SAAS平台分为两种模式:
- 面向个人用户的SAAS云服务平台,例如:各类记账本,微软的云端world等等
- 面向企业的SAAS云服务平台,例如:云端OA系统等等
那我们就有疑惑了,面向企业的SAAS平台是如何做到,大量的数据存在一起,还不出错的?一般而言也有以下X中方式
最简单的就是面向个人的平台是通过个人来区分用户的,那么面向企业的平台只要通过企业的标识(可能是uuid),在查询数据时从之前的select * from table where userid=?
变为select * form table where userid=? and companyid=?
就行了,大部分的业务逻辑其实跟个人SAAS平台一致。
上面介绍的是多租户的共享数据库共享数据表的做法,接下来我们介绍共享数据库独立scheme的思路
首先请求过来的时候会带上租户标识,然后通过拦截器拦截该请求,并获取到标识,动态的创建DynamicDataSource,这样子后续对DB的操作就可以通过拦截器与租户标识确定,最终实现共享数据库独立scheme。
原型分析的理念是指在获取一组基本需求之后,快速地构造出一个能够反映用户需求的初始系统原型。让用户看到 未来系统的概貌,以 便判断哪些功能是符合要求的,哪些方面还需要改进,然后不断地对这些需求进一步补充、细 化和修改。依次类推,反复进行,直到用户满意为止并由此开发出完整 的系统。 简单的说,原型分析法就是在最短的时间内,以最直观的方式获取用户最真实的需求。
如标题所示,其实通过观察原型上的字段,对数据库进行初始建模,然后再根据业务情况进行精准建模,是一个很好的选择。
首先我们需要知道一个程序就是一个可执行的文件。
而一个进程呢,就是一个可执行文件文件的执行实例。
java采用的是单线程的编程模型,如果代码中只有main线程执行程序,但是并不代表jvm虚拟机是单线程的。比如GC就是由一个线程专门执行的。
其中Native Interface一般调用的是c或c++的代码,这也是我们在追踪源码的时候发现标有native的方法却看不到实现的原因。
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用他的任一方法和属性;这种动态获取信息以及动态调用对象方法的功能,称之为java语言的反射机制。
ClassLoader在java中有着非常重要的作用,它主要工作在class装载的加载阶段,其主要作用是从系统外部获得class二进制数据流。它是java的核心组件,所有的class都是由classloader进行加载的,ClassLoader负责通过将class文件里的二进制数据流装载进系统,然后交给java虚拟机进行连接、初始化等操作。
我们可以自定义classloader,然后通过自定义的classloader加载非classpath路径下的类。这样就可以实现网络加载,或对class文件加密,后在这个classloader中解密等操作。甚至于可以修改classloader将要加载的二进制流,从而实现AOP的效果!
首先我们需要明确装载指的是将字节码文件加载到内存生成类class对象的全部过程,而加载时是这个过程的第一步
为什么要用双亲委派机制呢?因为为了保证一个类对象只会被加载一次,内存空间是很宝贵的,通过委派机制,每一个类加载器都是各司其职,不会出现一个类class对象在内存中被加载两份的情况。
ClassLoader的loadclass方法可以控制是否链接这个类。
而forname创建类对象的时候,会初始化这个类,即执行类变量的复制和静态代码块。
总结:
- Class.forName得到的class对象是已经初始化完成的
- ClassLoader.loadClass得到的class对象是还没有连接的(xxx.class也是调用的loadclass加载类class)
当我们知道这两种加载类的方式后,我们就能明白为什么加载mysql驱动需要用forname,而spring懒加载其实就是通过loadClass实现的。延迟加载能更快的启动ioc容器。这样子就能加快项目加载速度。
计算机内存可以分为
- 内核空间
- 用户控件(我们的jvm使用的是这部分空间)
我们说的java内存模型指的就是RuntimeDataArea。
从线程私有与线程共享的角度来分类的话,分类图如下:程序计数器一定是私有的,因为cpu的抢占式调度,所以在执行字节码时,一定需要记录字节码执行到哪一行了。
根据线程是否共享可以分为:
- 线程私有内存空间
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 线程共享内存空间
- 方法区
- jdk1.7之前方法区的实现在堆内存的永久代中
- jdk1.8之后方法区的实现在元数据空间中
- 元数据空间与永久代最大的区别是,元数据空间使用的是系统内存,而永久代使用的是jvm内存
- 堆内存
- 常量池也在堆内存中是1.6之后将永久代中的常量池移动到堆内存中的
- 方法区
堆内存空间是被所有线程共享的一块内存区域,堆内存在虚拟机启动时创建,该内存空间的唯一目的就是,存放对象实例,几乎所有的对象实例都在这里分配内存。
java对是垃圾管理器管理的主要区域,所以java堆也被称为GC堆。
因为现在GC回收期都是采用分代收集算法,所以java堆还可以细分为:
- 新生代
- 老年代
- -Xms:初始java堆的大小,如果堆的值超过最小值就会扩容到最大值,但是一般情况下,最大值与最小值是一样的,防止扩容产生的内存抖动!
- -Xmx:最大java堆的大小
- -Xss:规定了每个虚拟机栈的大小,一般情况下256k是足够的,如果太大会影响并发量,毕竟栈是线程独有,栈越大,相同内存情况下,独立栈的数量就越小,并发数也就越低,因为一个线程有独立的栈内存与之对应
使用方法为java -Xms128m -Xmx128m -Xss256k -jar xxx.jar
lineNo指的是当前执行的行号。
找到原图放大看!
程序计数器的作用有:
- 当前线程所执行的字节码行号指示器(注意---程序计数器只是逻辑计数器,不是物理计数器!)
- 字节码执行的逻辑就是,在字节码执行时,改变计数器的值来选取下一条需要执行的字节码指令,包括分支、跳转、异常恢复等等。
- 由于jvm的多线程,是通过线程轮流切换,并分配处理器,在任何一个确定的时刻,一个处理器只会执行一个确定的操作,因此为了线程切换后能回到正确的位置继续执行,所以每条线程都需要有一个独立的程序计数器,各条线程的程序计数器互不影响。独立存储。我们成这类内存是线程私有的内存。
- 如果线程正在执行一个java代码,那么程序计数器记录的就是虚拟机字节码指令的地址,如果正在执行的是native方法,那么这个计数器的值为Undefined。
- 由于只是记录行号,程序计数器不必担心内存泄露的问题。
首先栈是线程独有的,每个线程创建的时候都会创建自己的栈空间,而栈空间包含多个栈帧,栈帧的创建是由于方法调用引发的,当线程执行一次方法调用时就会创建一个栈帧,然后将建立的栈帧压如虚拟机栈中,方法执行完毕的时候才会将栈帧出栈,而栈帧包含局部变量表、操作数栈、动态链接、返回地址。局部变量表主要是用于存储局部变量的,它通过索引去访问值,而操作数栈是用于保存临时变量的,方法刚开始执行是操作数栈是空的,在执行过程中会有各种字节码指令,往操作数栈中存取数据,操作数栈也被称为一个操作栈,是一个后入先出的栈,在编译后操作栈的深度就已经被确定了。
在我们喜爱的递归函数中如果设计不当就会出现栈溢出异常。那为什么会出现这种情况呢?因为线程当前执行的方法所对应的栈帧必定位于java栈的顶部,而我们的递归函数不断去调用自身,每次调用自身都会创建一个栈帧,由于JVM虚拟机栈的深度是确定的(可修改),递归如果过深,栈帧深度超过虚拟机栈的深度就会出现栈溢出异常。
解决这个异常的思路主要有:
- 修改JVM虚拟机栈的深度
- 限制递归次数
- 有循环代替递归
首先我们需要知道什么样的情况叫做重入
从互斥所(排它锁,没竞争到锁的线程会被阻塞)的设计上来说,当一个线程试图操作由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况就叫做锁的重入
synchronized(this){
...一些逻辑
//此时当前线程已经获取到锁资源了,然后再次
synchronized(this){ //内层的synchronized依旧能获取到当前锁,所以说synchronized是一个把可重入的锁。
...一些逻辑
}
}
简单的总结一下可重入的锁,其实就是当前线程已经获取到对象锁之后,依旧可以在同步方法内继续获得锁资源。
其实我们通过redis实现的分布式锁(李卫民老师讲的那种)就是一种不可重入的锁,如果在锁的代码块内获取锁就会导致程序一直阻塞下去。
自旋锁产生的契机是因为,cpu线程切换比较消耗性能,并且很多场景被锁定的资源只需要极短的时间,这样的情况下,如果让其他线程进入锁池,等待竞争锁其实是很浪费时间的,还不如让这个线程等待一小会,然后继续执行,类似于while(true){}的方式让线程等待我们可以想象旋转,当线程没有获取到锁的时候,他就自己旋转一会儿,等待锁的释放,然后继续执行。此时"旋转"的那个线程,相当于是一直在运行。
普通自旋锁会有一个问题,就是每次旋转的次数是固定的,但是在日常cpu运行中,很难确定需要旋转多少次,所以就有了优化方案,自适应自旋锁。如下所示
首先锁消除是Java对锁的一种优化,对应的另一个极端就是锁粗化
一般情况下,我们都是尽可能的减小同步代码块中的逻辑,以便更快的执行。但是日常开发中一段代码有可能反复使用一个对象锁,甚至在循环中使用锁,那么即使没有线程竞争,大量的锁操作也会造成不必要的性能损耗,所以我们想到的方法就是通过扩大加锁的范围,避免反复加锁和解锁。
- Partition tolerance(分区容错性)
- 我们是要做分布式系统所以P原则一定会成立,因为网络是不可靠的
- Consistency(一致性)
- 什么叫做一致性呢?意思是,写之后的读操作,必须返回写操作后的值。
- 我们在使用tomcat集群模式的时候,用户身份cookie信息就有可能不满足一致性的要求,因为在没有引入redis的时候,我们访问tomcatA写入cookie后,再访问服务器有可能访问的就是tomcatB,于是就出现刚才能用的cookie信息,在tomcatB服务器用不了了,这就是没有考虑到数据一致性产生的问题,数据一致性的出场一定伴随着"有状态的数据,即现在用了,等会这个用户还要用!"。
- Availability(可用性)
- 可用性的意思就是只要收到用户请求,服务器就必须给出回应。
一致性和可用性,为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。
如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性不。
如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立。
综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那就没法做到一致性。
我们在使用mysql的时候,肯定不能直接去操作mysql服务器,我们要通过客户端去操作mysql服务器,在linux中,我们可以通过mysql自带的命令行客户端操作mysql相关行为,在视图层面,我们可以通过sql yong等等图形化客户端操作mysql,在代码层面我们想要操作mysql服务器,肯定也是通过客户端的,而mysql-connecter-java其实就是java语言作为客户端操作mysql的情况。所有的服务器都是通过客户端进行操作的,客户端的形式一般会有:
- 安装服务器时自带的客户端,比如mysql客户端,redis客户端,都是在安装服务器后自带的,用于快速测试操作对应的服务器。
- 我们为了便于操作一般会有图形化界面的客户端,比如sql young再比如redis客户端
- 最终面向客户时,肯定不是通过前两种方式对服务器进行操作的,一定是客户通过点击图形化界面发送请求,后端根据客户请求调用编程语言,编程语言根据服务器提供的编程语言级别的客户端,再去操作服务器。可以说前两种都是为了方便开发人员调试,第三种才是我们开发中最值得学习的。
上面我们讲到的是客户端,那么对这些客户端的管理是通过
其实在我们绘制时序图等的时候,会有访问服务器获取资源的说法,其实本质上访问服务器获取资源,是访问服务器对外暴露的接口,通过接口去获取服务器中保存的资源。
- 认证(authentication): 验证用户的身份;主要是判断当前用户是谁,是否登录等等。
- 授权(authorization): 验证用户的访问权限。判断当前用户是否拥有当前接口的访问权限。
传统的多点登录系统中,每个站点都实现了本站专用的帐号数据库和登录模块。各站点的登录状态相互不认可,各站点需要逐一手工登录。
单点登录,英文是 Single Sign On,缩写为 SSO。
多个站点(192.168.1.20X)共用一台认证授权服务器(192.168.1.110,用户数据库和认证授权模块共用)。用户经由其中任何一个站点(比如 192.168.1.201)登录后,可以免登录访问其他所有站点。而且,各站点间可以通过该登录状态直接交互。
我们自己开发权限拦截的时候,是通过filter,拦截所有需要登录的请求,然后在拦截器内部获取到当前访问的url,通过数据库查询出当前url对应的权限点。然后在filter中获取(查询)出用户的相关身份权限信息进行匹配。如果如果当前用户的权限点(用户查询角色,再查出角色包含的权限点)不包含访问该url需要的权限点 ,那就说明无权限访问,在filter中根据httpservletresponse返回权限不足页面或权限不足json信息(该方法返回值为void)。
一般而言在使用filter进行拦截的时候,先配置需要拦截的路径(可以为/**),然后在拦截器内根据数据进行判断用户是否有权限访问,比如我们根据当前路径在权限表中进行搜索,如果返回为null,说明是无需认证就能访问的页面,核心**就是根据数据库存放的权限数据,进行if else判断是否调用后续逻辑!未拦截的路径一定是无需认证就能访问的路径,拦截的路径一定是需要认证但不一定需要授权就能访问的页面。比如study52的列表页,不认证是无法访问的但是这个页面对用户的权限没什么要求。
一般白名单的url是在配置文件或filter中直接配置好的,这部分url是需要认证但无需授权就能访问的比如权限不足页面或权限不足url。filter不拦截的请求是无需认证就能访问的,比如京东首页。
- 无需认证就能访问的资源---如京东首页
- 需要认证但无需授权就能访问的页面---如个人中心
- 需要授权才能访问的页面---如study52的藏宝阁页面
首先我们需要明确一点,OAuth2.0是一个协议,协议就是一种规范,规范的实现方式有很多种,我们可以使用spring security、shiro实现、自己通过servlet实现。
简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
Oauth2.0协议的宗旨是,**客户端不可以直接访问资源服务器,必须引入授权层!**什么是客户端要辩证的去看,我们想要访问用户在微信的数据的时候,我们得页面或手机app就是客户端,他们对应的服务器就是第三方应用。当我们自己开发的后端应用需要访问资源服务器的时候,我们自己开发的后端应用服务器,其实也就成了第三方应用!!!
分辨是不是第三方应用主要是这样区分:"以当前应用为基准,当我们在当前应用中访问的数据是用户之前在别的服务器上就已经存在的数据的话,那当前应用相对于客户与客户之前存储的数据而言,那它就叫做当前应用!!如果客户访问的是在当前应用服务器上的数据的话,那么当前应用就不是第三方服务器了,这里比较难理解的一点事,随着请求数据的不同,那么当前应用就有可能便成为第三方应用服务器,因为对于用户和用户在别的地方的资源来说,当前应用就是第三者啊"。
Oauth不代表着单点登录,单点登录是单点登录,Oauth只是一种认证授权标准,可以用于单点登录,也可以用于多点登录。
**当需要访问其他系统的资源的时候,都可以走OAuth协议。**比如系统中的微服务A需要访问微服务B的某些信息时,可以通过Oauth协议来完成。服务A就相当于第三方应用,服务B就相当于资源服务器。客户端就是第三方应用,比如客户通过浏览器访问
令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异。
- 令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
- 令牌可以被数据所有者撤销,会立即失效。以上例而言,屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销。
- 令牌有权限范围(scope),比如只能进小区的二号门。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
- 令牌的使用者是第三方应用,而密码的使用者是资源持有用户。
注意,只要知道了令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄漏令牌与泄漏密码的后果是一样的。 这也是为什么令牌的有效期,一般都设置得很短的原因。
==在oauth协议中,任何模式最终的目的都是获取一个token,而根据这个token就能获取到当前用户的相关权限等信息,token就是一个身份令牌,跟我们传统项目中cookie存的jsessionId(登录后的jsessionid)是同样的功能,他们都是一个身份标识,只不过token的获取是通过oAuth协议来完成的,比jsessionid获取的更为安全和繁琐。根据这个信息我们就可以完成一系列的认证、授权操作。==
当我们获取token之后,就相当于完成了认证,即获取到了当前用户的身份,接下来就是授权了,即对当前用户的身份背书的访问权限进行判断,最终决定用户是否有访问权限。
想要玩的起授权码模式,最少得有三台服务器,一个认证服务器,一个资源服务器,一个消费资源的服务器 ,就像是微信登录一样,用户A通过微信来登录我们开发的网站,我们网站去访问微信的认证服务器,最终去访问微信的资源服务器,这其中就有三个服务器参与本次认证与授权。其中token与code都是先流转到服务器,(其实认证服务器与资源服务器可以是同一台服务器,但是吧对于我们oauth2的流程是不会有任何影响的,依旧是code换token不是密码换token)
反观密码模式,本质上并不需要三台服务器的参与,只需要认证服务器与资源服务器就可以玩的起来!
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权(是否认证,以及认证范围的页面是由认证服务器提供的)。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
下面是上面这些步骤所需要的参数。
A步骤中,客户端申请认证的URI,包含以下参数:
- response_type:表示授权类型,必选项,此处的值固定为"code"
- client_id:表示客户端的ID,必选项
- redirect_uri:表示重定向URI,可选项
- scope:表示申请的权限范围,可选项
- state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
下面是一个例子。
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
HTTP/1.1
Host: server.example.com
C步骤中,认证服务器回应客户端的URI,包含以下参数:
- code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
- state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
下面是一个例子。
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz
D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
- grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
- code:表示上一步获得的授权码,必选项。
- redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
- client_id:表示客户端ID,必选项。
下面是一个例子。
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
E步骤中,认证服务器发送的HTTP回复,包含以下参数:
- access_token:表示访问令牌,必选项。
- token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
- expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
- refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。(刷新token必须保存在客户端的服务器中,这样即使通过抓包获得了access_token,也会在短时间内失效,当access_token失效的时候,服务端就可以自动利用刷新token去资源服务器获取数据,从而实现,token失效,但是用户不用重新首选的效果!)
- scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
下面是一个例子。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
从上面代码可以看到,相关参数使用JSON格式发送(Content-Type: application/json)。此外,HTTP头信息中明确指定不得缓存。
注意,只要知道了令牌(access_token),就能进入系统,access_token肯定在应用服务提供商的某一块内存中对应着用户数据,可能在作为redis的key去匹配他的值等等,access_token就相当于确定了用户身份,能够取得用户的数据资源。系统一般不会再次确认身份,所以令牌必须保密,泄漏令牌与泄漏密码的后果是一样的。 这也是为什么令牌的有效期,一般都设置得很短的原因。
其他授权模式可以查看阮一峰老师的博客 http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html?tdsourcetag=s_pctim_aiomsg
授权码模式:
密码模式:
简化模式:
客户端模式:
主要区别有:
- 授权码模式中,认证页面是认证中心提供的,所有的通过授权码模式认证的服务都是共用这一套页面!而密码模式的认证页面是由每一个不同的服务自己提供的,可以根据服务做不同的定制。
- 密码模式与简化模式比较相似,但是简化模式只有js的控制权,所以简化模式的token是直接传递给前端的,很容易被截获。而密码模式的token是返回给后端的,会安全很多!
- 授权码模式与密码模式最大的区别就是,授权码模式获取到code的时机并换取token是在服务器内部完成的,而密码模式直接调用认证服务器的接口获取token。相当于少了认证code换区身份token的步骤。
我们获取code的方式其实是通过前端js获取,然后给我们的服务器发送请求,再由我们的服务器根据code去认证服务器换取token的。只有这样的情况下,前端才可以获取到最终的token。如果由后端直接接受code,那么发送认证请求的前端就会找不到,因为http协议是无状态的!
这里不能由前端直接根据code发送请求主要的原因可以从微信的官方文档得知:
尤其注意:由于公众号的secret和获取到的access_token安全级别都非常高,必须只保存在服务器,不允许传给客户端。后续刷新access_token、通过access_token获取用户信息等步骤,也必须从服务器发起。
请求方法
获取code后,请求以下链接获取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
上面的secret与url中的appid,其实就是oauth的clientid与clientSecret只是名字不同而已。
刷新令牌最大的作用是,用户无需重复登录,就可以在token失效后获得新的令牌。为了安全起见token的过期时间一般都比较短,所以在token失效后相当于用户的认证消息过期,此时通过刷新token就可以做到用户在不重新输入用户名密码的时候,第三方应用获取新的token。
这里说到第三方应用,突然想到,第三方应用指的不是一个服务器!很有可能是一套微服务架构,就比如我们开发的程序要做微信登录的时候,对于微信和用户在微信上保存的资源来说,我们整个微服务架构都是第三方应用。
会生效,然后将标签的属性放在从而动态替换上图中的${deploy.type},实现不同的打包命令参数,使用的配置文件目录是不一样的!这点非常重要,想想之前把所有配置写在一起上线和调试交互进行时的痛苦!!!
其实就像是httpclient一样,http服务器通信是通过http协议,只要我们能够模拟http协会向http服务器发送请求就能与http服务器进行通信。同样的ftp服务器使用的是ftp协议,它与http协议都是TCP/IP协议,所以只要我们能够模拟ftp协议就能与任意种类的ftp服务器进行通信了。就像我们通过httpclient与tomcat、netty、Nginx、或php的服务器,通信一样。通过ftpclient就可以与fastdfs、vsftpd等等进行通信。因为协议是标准规范,所以工具类是能够通用的,即我们写好的ftpclient不需要修改就能与多种ftp服务器进行通信,就是想httpclient一样无需修就能与各种语言写的http服务器进行通信。
首先我们需要对在进行领域驱动设计的时候,会根据三层架构,将代码分为视图层、业务逻辑层、持久层。每个层都会有自己的侧重点。
- 视图层侧重点---前端界面的展示。视图层通过不同前端界面的展示,去聚合不同的领域模型,生成新的VO对象去展示。
- 业务逻辑层侧重点---领域对象,领域模型才是我们真正要关注的业务层面(service层)的对象。也就是说我们的业务数据库怎样去设计,都取决于领域模型!!!
- 业务模型的设计最终需要反映到我们的持久层,也就是反映到我们的数据模型上面(这也是mybatis-code-helper提供了从DataObject到数据库建表的功能,因为在领域驱动设计的时候,先出来的就是DomainObject然后是DataObject,然后才是数据表,所以很有必要增加从DataObject到数据表的功能,这也是我们经常性的做法)。数据模型的设计重点是:效率设计、边界设计、分库分表设计等等,持久层的设计就是为了效率,以前可能还会考虑存储空间,现在完全不需要的。
具体的可以听龙虾三少的那个秒杀收费课程。
网页静态化就是通过生成HTML文件的方式,让用户尽可能多的访问HTML文件,而不是老是调用数据库生成网页。这样可以降低服务器的负载。
页面静态化做到的效果是,将一些动态的页面,提前生成html,从而达到动态页面静态化的效果!
无论是关系型数据库还是非关系型数据库中查询数据的时候都会存在以下两个问题:
- 动态构造查询条件进行查询(查询条件自由组合时推荐)
- 构造精准的多个查询条件进行查询(查询条件非常确定,为了维护性和少量性能考虑应该使用精准查询)
在日常开发中,我们可以通过精准的条件查询,查询出我们想要的内容,比如通过性别与年龄查询出符合的人群,但是在真正的开发中,很少会要求我们同时传递性别与年龄(非必填项),在处理这种sql的时候,我们如果根据不同的条件执行不同的sql,显然开发量与维护量就很大了,所以需要动态构建查询条件,然后用动态查询条件进行查询。
所以在我们有了findByGenderAndAge()方法之后,我们还需要findAll(动态查询条件对象)。在开发时,如果明确某几个参数必须传递,那么就通过"精准查询"的方式进行查询。如果并不明确参数是否毕传那我们尽量通过动态查询条件去替换上面提到的"精纯查询",这样做的好出是,复用性得到了提高,但是维护性可能会有下降,不过完全可以接受。
尤其在有很大查询条件,并且需要支持自由组合查询条件的情况下,我们一定要通过拼接动态查询条件方式,进行开发。
Exchange交换机就负责消息的转发。
上图我们可以知道通过不同的交换机可以将消息转发到不同的队列中,每个消费者(consumer)会监听自己需要消费的队列,消息是由producer发送给交换机,由交换机将消息按照交换机设定的策略转发给指定的队列(上图中的queue),消息队列(queue)相当于是消息暂存的一个地方。当我们把消息发送到队列,此时如果consumer正在监听队列的话,queue就会将消息发送给consumer。如果consumer没有监听队列,那么消息就会暂存在queue中等待被消费。
消息发布接收流程: ----发送消息(第一大步)----
-
生产者和Broker建立TCP连接。
-
生产者和Broker建立通道。
-
生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
-
Exchange将消息转发到指定的Queue(队列)
----接收消息(第二大步)----
-
消费者和Broker建立TCP连接
-
消费者和Broker建立通道
-
消费者监听指定的Queue(队列)
-
当有消息到达Queue时Broker默认将消息推送给消费者。
-
消费者接收到消息。
上面关于通道与连接的区别是,通道可以看成是大的TCP连接中的小连接,就是一个虚拟的连接,这样做的好出是,不同的通道之间可以实现回话隔离。这也是rabbitmq可以支持多个消费者与多个生产者通信的机制。
根据上面的流程我们可以知道,消费者只是监听队列,不用考虑队列从哪里或那种模式(主题模式、直连模式等等)受到的消息,消费者的代码中只需要做一件事,监听队列,然后处理消息。至于消息是如何发送到队列中的,这是生产者通过交换机发送的,并不是消费者端需要考虑的问题,这样也符合单一职责原则。
首先主题模式完全可以实现发布订阅者模式与路由模式的功能。
主题模式与路由模式最大的区别就是,路由模式在绑定交换机与队列的时候是通过一个确定的routingKey来实现的,发送消息也是通过同一个交换机根据不同的routingKey发送到不同队列中的。
而主题模式与路由模式相比,代码上唯一的区别就是在绑定交换机与队列时,routingKey是非确定的,是一个模糊值,这样在发送消息时,通过一个确定的routingKey发送,然后通过规则匹配,找到最终要将消息发送到的那个队列进行发送。
明显的看到一个在队列绑定是使用的是确定的routingKey一个是模糊的routingKey。
在日常开发中我们会写大量的if语句,应当将范围更广的条件,放在最后的if语句,比如x>5之后再写x!=3,因为如果将x!=3写在前面,那么x>5是不可能被触发的。
异常处理指的是,如果用统一的异常处理的方法,来处理我们项目中每一个模块代码当中的异常。在java代码中,捕获异常是在当前方法内部捕获,或是在方法的调用处捕获,但是再开发web应用时,每个地方都使用try{}catch{}会让代码十分的冗余。所以现在有一中不加try---catch 的方法,并且能在一个地方捕获各个controller发生的错误。一般而言我们没有统一的处理异常,那么就会报出服务器500!
具体的异常处理问题详见
黑马综合\学成在线\day03 CMS页面管理开发\资料
加入通用异常处理后,我们代码的书写逻辑为,先通过判断去抛出各种异常,然后再书写执行正常逻辑的代码。因为抛出异常后,代码就会阻断执行。
统一的异常处理类可以捕获controller、service甚至dao的异常。
一般而言我们需要看最顶层抛出异常的地方进行排查。异常方法调用栈的顺序是,第一行与当前异常有关系的方法在异常信息的最底下,最上面的第一行代表抛出异常方法调用终结的那一行。
- servlet相关知识(如原生拦截器实现跨域访问,session相关知识等)
- web服务器开发的一些特定场景的思路,比如如何实现后端表单防止重复提交等
我们在开发任意一个系统的时候,都会存在状态的变化,只要是存在状态的变化势必会反映到数据库的增删改查。比如电商的下单操作,其实本质上就是查询库存(查),判断库存是否满足当前下单量(内存逻辑),对当前库存进行减少(改),然后生成订单表与订单详情表(曾),订单详情表记得需要做商品详情的快照!
处理用户登录的思路为,系统校验成功用户的登录名以及密码后,将用户名对应的用户数据存放在内存空间中,以K-V键值对的形式进行保存。比如在javaEE中,用户登陆成功会创建一个session空间,这个session空间也是一个K-V键值对的形式,K就是字符串JSESSIONID,V是一个map。登陆成功后我们将对用SESSION空间的字符串写回浏览器的cookie中,然后浏览器在访问该域名的时候,就会自动带上相对应的cookie,我们在服务端校验cookie是否有效(即这个cookie能不能从session集合中取出一个session对象,并且这个session对象中有保存用户登陆成功后的信息),如果能够校验成功,那就代表着用户是登陆状态。这就是无状态的http协议,保存用户登录的套路。
这里值得一提的是,用户的会话保持,并不意味着用户就得登录,登陆成功后会向session空间中存储一个用户信息的K-V键值对,但是如果没有登录,这个session空间还是存在的,对用的就是目前操作的客户端浏览器,要把会话保持跟用户登录进行区分,只要有会话保持我们就能做很多事情,比如一次性验证码,比如后端防止用户重复提交,等等。
总结一下,判断重复第一时间就要想到count,如果对数据有要求可以将count的逻辑由数据库转移至redis中,比如某个人秒杀商品成功,那就在redis记录商品id与用户id,然后判断是否重复秒杀就用商品id与用户id进行判断即可(redis用set数据结构进行存储,key为用户id,set集合的内容时秒杀成功的商品列表。用if is exist判断是否有key'为用户id,如果有的话判断这个key对用的列表是否有商品id)
一般而言将需要查重的字段在数据库中count一下,有几个字段就通过AND连接,看看返回的int值是否大于1。
如果是查看是否存在数据,需要count==0。
在查询数据的时候,如果存在xx=null这样的情况,肯有可能一条数据都查不出来,所以一般通过...内容...进行处理,要不然就是在pojo类中,直接给属性赋予初始值。
以下为记住我功能实现
注意,记住我功能的实现,是存在第一次登录与第二次登录的情况的,首次登录勾选记住我,然后回写用户名密码到客户端的cookie。
判断是否携带登录信息的逻辑应该写在过滤器中!
- 引入redis客户端依赖
- 对redis客户端进行配置,为了能够与redis服务器进行交互
- 封装redis连接池工具类
- 缓存我们认为需要缓存的方法
- 实际中我们一定需要明确对哪些方法进行缓存,以及我们需要对哪些方法进行缓存,而不是说随便一个方法都需要缓存
- 比如说实时性要求非常高的数据,就不一定适合缓存。有些请求并没有什么数据库操作,当然也不需要缓存,如果数据使用并不频繁也不需要缓存,是否缓存是根据业务决定的!权限框架的缓存对我们并不是很透明,所以我们自己实现缓存是很有必要的。
首先我们清楚,在用户登陆成功后,会将用户信息保存到一个内存空间中(session空间,request空间,redis空间),然后当下一次请求来的时候,会带有一个标识,以便服务器找到那个记录用户信息的内存空间(因为所选取技术不同,所以找寻的思路也不相同)。
通过以上逻辑我们就可以明白了,想要知道用户是否登录,只要在拦截器或者过滤器中,根据请求中的标识,去寻找保存用户的内存空间,如果能找到,则说明用户已经登录,如果找不到就重定向到登录页面,当然了具体的做法很多,但是思路的核心就是在服务端找到这个用户空间才能说明用户已经登录了。
比如:
package com.mmall.common;
import com.mmall.model.SysUser;
import javax.servlet.http.HttpServletRequest;
public class RequestHolder {
private static final ThreadLocal<SysUser> userHolder = new ThreadLocal<SysUser>();
private static final ThreadLocal<HttpServletRequest> requestHolder = new ThreadLocal<HttpServletRequest>();
public static void add(SysUser sysUser) {
userHolder.set(sysUser);
}
public static void add(HttpServletRequest request) {
requestHolder.set(request);
}
public static SysUser getCurrentUser() {
return userHolder.get();
}
public static HttpServletRequest getCurrentRequest() {
return requestHolder.get();
}
public static void remove() {
userHolder.remove();
requestHolder.remove();
}
}
通过以上代码我们可以知道,在开始一次请求的时候(比如就是调用登录controller的逻辑中),向threadlocal中存入用户登录信息以及当前请求的request对象,这样子在当前请求的任意一个位置,都可以通过RequestHolder(案例中,带泛型)这个类快速获取这两个对象。
至于销毁当前threadlocal对象的方法,可以在拦截器、过滤器的最后进行调用,这也是AOP的**。
threadlocal的优点有:
- 简化逻辑
- 减少数据传输
- 性能高
最原始的RBAC模型我们已经知道了,但是如果想要对RBAC模型进行扩展的话,就需要添加更多的关联表。比如我们想要对权限点进行分类,就需要一个权限类别表,权限类别表的用处在于展示权限时引入分类的功能。权限类别表并不参与原始RBAC模型中权限的代码逻辑,也就是说角色权限表,依旧存的是具体的角色与具体的权限点,而不会牵扯权限分类表。
所以我们在进行扩展的时候,一定要保证核心的逻辑不动(非代码不动),除非万不得已!万不得已!万不得已的时候,才会考虑修改我们之前想好的核心逻辑!!!
并发的瓶颈就在数据库,我们优化最主要的方向就是减少对数据库的访问,而减少对数据库的访问最有效的手段就是加上缓存。通过各种各样的缓存,比如页面的缓存,URL的缓存,对象的缓存。
总结一点,凡是有状态的数据,都应该从原本tomcat的内存中抽离出来,放到一个第三方"内存中",以便任何一个tomcat都能访问到该数据。
那什么叫做有状态的数据呢?我们都知道http协议是无状态的,两次请求都是独立的。有状态的数据是指,多次无状态的http请求,要用到同一个数据,那么这样的数据都是有状态的,比如找回密码的uuid。
一般而言我们的鉴权逻辑应该放在过滤器或拦截器中,但是鉴权成功获取用户信息后,这个信息该怎样传递到controller,service层呢?其实就可以用到ThreadLocal进行传递,这样做既优雅又高效。
因为http协议是无状态的,session、cookie、token都是为了保存用户状态而产生的一项技术。
一般而言cookie中存放着sessionId会自动从浏览器发送到服务端,服务端自动根据sessionId查找对应的session空间。但是也会碰到客户端禁用cookie的情况,这时候只要把sessionId存方法到客户端的任意位置,然后在发起的请求是带上sessionId,服务端相应的进行解析获取session的内容就行了。比如url重写就是把sessionId直接放在url上,随着请求直接发给服务端。
现在大多都是Session + Cookie,但是只用session不用cookie,或是只用cookie,不用session在理论上都可以保持会话状态。可是实际中因为多种原因,一般不会单独使用session或cookie。其实知道了套路后自己定义一个全局map,然后登陆请求时生成一个唯一表示,将数据存进map中,客户端每次请求带上这个key,依旧能达到保持回话的目的,但是用成型的技术,肯定好,因为已经被优化了无数遍了!!
token一般是将用户信息放在客户端,然后每次请求带上令牌,服务端进行cpu运算解密就行了。比如著名的JWT令牌,就是token的理念。
session保存回话的原理如下:(根据时间排序)
- 服务器正常启动---session未创建
- 客户端发送请求到服务端---服务端通过request.getSession()创建一个session,以及自动保存sessionid
- 对于本次请求,服务端在响应头中会添加set-cookie字段,内容就是上一步保存的sessionid,本步骤也无需我们参与tomcat自动帮我们完成了
- 客户端再次访问会带上jsessionid,服务端根据jsessionid找到第2步骤中创建的那个session空间,拿到本次回话的用户信息,从而事件http协议的状态华
- 根据用户名获取问题
- 根据用户名,问题以及问题答案换取token
- 根据用户名以及token,修改新的密码
套路很简单,主要需要解决的是横向越权的问题。思路就是通过唯一的用户名只能找到唯一的token,只有客户端传递的token与在第二步中存到缓存中的token一致时,才能修改密码。这样就能保证客户端获取的token只能用于回答正确问题的那个用户名从而保证不会发生横向越权的问题。
其实手机验证码找回密码也是这么个套路,核心思路是让token与某一个用户名绑定起来,然后修改时校验客户端传递的token与服务端保存的token是否一致,一致的话就修改密码(相应敏感信息)。
首先我们要知道分布式架构下为什么不能使用,上图!我们可以看到请求一个带有getSession的controller的时候,出现了又有cookie也有setcookie的情况。复现步骤为:
- 启动服务器打开本页面,发现响应中有set-cookie字段用于设置cookie
- 刷新页面后发现每次请求都会带有cookie中的jsessionid字段
- 关闭服务器,让服务端的session失效
- 开启服务器,再次访问当前页面,就会出现cookie与setcookie同时出现的情况
以上情况说明因为session是存在服务器内部的,当服务器根据jesssionId没有找到对应的session空间时,会重新setCookie给客户端一个新的值,这设计在单tomcat架构中没有问题,但是分布式情况下,就会导致,用户在A-Tomcat中登录后回去到jsessionid,再被负载到B-tomcat中时,B-tomcat因为找不到当前jsessionId对用的session空间,会在响应中添加set-cookie字段,导致用户的登录状态失效,跳转到登录页面。用户登陆成功后,有可能又被负载到A-tomcat,这时候用户的jsessionid又找不到对应的session空间。以上客户就很难受了,肯定不在你的网站上玩耍了!
所以解决的思路就是,别使用session了,将用户信息保存到redis服务中,将redis的key写入set-cookie中,这样就相当于在使用cookie的情况下,绕过session空间。从而实现多服务器的单点登录。
使用cookie而不是客户端传递redis的key的好出在于,可以简单的设置过期时间,domain等等,而且登录操作给前端屏蔽,防止出错,后端也更可控。重要的是可以对cookie设置httponly等属性,这样就保证脚本不能读取当前的cookie信息,浏览器也不会把cookie属性发给任何第三方,能做到一定的安全性。
在分布式架构下我们的回话保持是通过redis中的session信息与浏览器发起请求携带的cookie共同完成的,所以想要做到只要请求就让session失效时间刷新为当前请求事件的半个小时之后,就需要从redis存储session的失效时间与客户端保存cookie的失效时间入手。
一般而言分布式架构的session是存放在redis中的,我们只需要在过滤器中加入如下逻辑:
- 收到请求校验用户成功后将redis中用户信息的过期时间设置为半个小时之后。
- 修改客户端cookie的失效时间,依旧是设置为半个小时之后。
根据旧密码,如果校验成功,从session中取出用户信息,然后更新。要确保旧密码正确,需要保证是某个用户的旧密码,就必须传递用户名以及当前密码进行比对。不能只是传递一个旧密码在数据库中select进行查询。否则会大概率性的出现密码一样,从而发生横向越权问题。
注意点就是从session中取出用户信息,修改后更新数据库,核心就是不要相信客户端传递过来的参数,防止横向越权。
套路依旧是从session中获取用户详情,然后根据前端传递的值进行修改,再更新数据库,反写回session中。依旧是防止横向越权。
在web开发中,返回数据并不一定要通过springMVC的controller。我们在开发中,只要能够获取到本次的response对象,那就能够给浏览器写数据。不要因为学了框架,就忘记servlet规范。这一点在springMVC的拦截器中也有体现。
我们知道拦截器的preHandle方法,返回值是boolean,并且返回false的时候,并不会执行controller方法。这种情况下,我们需要返回false阻止访问,又要给客户端返回相应的错误信息时,就是通过response来实现的。注意需要先将response.reset()调用一下,否则会抛出一个异常。
在快乐商城中,我们对购物车的增删改查最终都会调用一个通用的方法,前端根据通用的返回值渲染页面即可。也就是说,选中商品、删除商品、给购物车中新增商品等等,都会先对数据库进行增删改查操作,然后调用通用方法,返回一个通用的对象,进行渲染。改思路主要的逻辑是:
- 服务端接收请求
- 根据请求对数据库进行增删改查
- 调用通用方法
- 通用方法直接通过对数据库的查询,以及对通用对象的封装,返回对应的数据
以上思路的核心就是,通用方法只从数据库中直接获取数据,与每个特殊方法具体的执行逻辑其实没有关联。他是通过,同一个数据库进行关联的,而不是通过代码进行关联。通用方法只干了一件事根据数据库中的数据,组装返回给前端的视图对象。数据库就相当于一个缓冲层,用于实现解开特殊代码与通用代码的耦合!
根据这点我们也可以学到,如果有这样一种业务场景,一个视图的呈现与与某些固定表数据是强相关的。并且这个视图中有很多操作(增删改查各一种这是必须的吧),那么就可以封装一个从数据库到视图对象的通用方法,然后在特殊方法中总是调用这个方法返回数据就行了。这种**不可避免的会在循环里写sql,不要过度抵触这样的做法,可以说这种情况下,循环里写sql就很优秀!
主要是两篇文章,来自知乎的文章
cache 是为了弥补高速设备和低速设备的鸿沟而引入的中间层,最终起到加快访问速度的作用。 而 buffer 的主要目的进行流量整形,把突发的大数量较小规模的 I/O 整理成平稳的小数量较大规模的 I/O,以减少响应次数(比如从网上下电影,你不能下一点点数据就写一下硬盘,而是积攒一定量的数据以后一整块一起写,不然硬盘都要被你玩坏了)。
作者:知乎用户
链接:https://www.zhihu.com/question/26190832/answer/32387918
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
来自csdn的文章
由于该知识点涉及到I/O操作,因此,在这里简单的对I/O操作做简单的说明。
冯诺依曼体系结构中,将计算机分为运算器、控制器、存储器、输入/输出设备。而运算器、控制器是CPU的组成成分(还有一些寄存器)。存储器则可以分为内存储器(内存)和外存储器(硬盘)。输入输出设备主要用来完成系统的I/O操作。I/O操作主要是对硬盘中的数据进行读和取。由于CPU的运算速度远远大于I/O操作,因此,当一个进程需要产生许多I/O操作时,会耗费许多系统资源,同时也不利于进程之间的资源竞争,导致系统资源的利用率下降。
由于CPU不能够直接访问外存(硬盘),而需要借助于内存来完成对硬盘中数据的读/取操作。想要完成此操作又不得不借助于I/0系统。但是,JAVA中的输入/输入流在默认情况下,是不被缓存区缓存的,因此,每发生一次read()方法和write()方法都需要请求操作系统再分发/接收一个字节,这样程序的运行效率必然会降低,相比之下,请求一个数据块并将其置于缓冲区中会显得更加高效。于是我们考虑可以将硬盘中的数据事先添加到预定义范围(大小合适)的缓冲池中来预存数据,待CPU产生I/O操作时,可以从这个缓存池中来读取数据,这样便减少了CPU的I/O的次数,提高了程序的运行效率。JAVA也正是采用这种方式,通过为基本流添加了处理流(缓冲机制)来减少I/O操作的次数。
JAVA中的write()方法和flush()方法是抽象基类OutputStream/Writer中的两个方法。其中,OutputStream类中的主要方法包括以下几个:
OutputStream抽象类中的方法 abstract void write(int n) 写出一个字节的数据 void write(byte[] b) 写出所有字节到数组b中 void write(byte[] b,int off,int len) 写出某个范围的字节道数组b中
b 数据写出的数组
off 第一个写出字节在b中的偏移量
len 写出字节的最大数量
void close() 冲刷并关闭输出流 void flush() 冲刷出流,将所有缓冲的数据强制发送到目的地。
read()方法和write()是线程阻塞的,也就是说,当某个线程试图向另一端网络节点读取或写入数据时,有可能会发生网络连接异常或者是服务器短期内没有响应,这将会导致该线程阻塞,同样地,在无数据状态进行读取,数据已满进行写操作时,同样会发生阻塞,这时,其他线程抢占资源后继续执行。如果出现此现状,读取到缓冲池中的数据不能够及时的发送到另一端的网络节点,需要该线程再次竞争到CPU资源才可正常发送。
还有一种情况,当我们将数据预存到缓冲池中时,当数据的长度满足缓冲池中的大小后,才会将缓冲池中的数据成块的发送,若数据的长度不满足缓冲池中的大小,需要继续存入,待数据满足预存大小后再成块的发送。往往在发送文件过程中,文件末尾的数据大小不能满足缓冲池的大小。最终导致这部分的数据停留在缓冲池无法发送。
这时,就需要我们在write()方法后,手动调用flush()方法,强制刷出缓冲池中的数据,(即使数据长度不满足缓冲池的大小)从而保证数据的正常发送。当然,当我们调用流的close()方法后,系统也会自动将输出流缓冲区的数据刷出,同时可以保证流的物理资源被回收。 ———————————————— 版权声明:本文为CSDN博主「篱萧雨」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_35271409/article/details/82057096
学习完以上的知识,再看下面这张图就清晰的多了为什么要调用flush是因为java读取文件时也存在一个缓冲区,当最后一波读取完毕后,并不能保证缓冲区写满,缓冲区未写满时不会将数据写入输出流中。所以需要调用flush()方法,将数据刷新到流中!
controller层中应该返回前端能拿到的数据,对数据的拼接也应该放在controller层中。controller层可以直接写与返回值有关的判断逻辑,如果逻辑与业务相关应该放在service层中。
service层只能调用自己的dao层,如果需要调用别的dao应该通过别的service进行。因为有可能方法会对dao层的数据做特殊处理,比如缓存。直接调用dao层会导致service的复用性变差,并且维护难度增加!!
首先这三个HTTP头部都是用来控制与缓存有关的内容。
Pragma和Cache-Control的优先级应该是后者优先级高(在HTTP/1.1协议下),因为在HTTP/1.0协议下是不认识Cache-Control这个头的,所以也谈不上什么优先级
一般而言我们现在使用的是Cache Control与Expires,他们两个有什么区别呢?
- Expires 表示存在时间,允许客户端在这个时间之前不去检查(发请求),等同max-age的效果。但是如果同时存在,则被Cache-Control的max-age覆盖。
- Expires 使用的是格林尼治时间,这样可能会产生服务器与客户端事件格式不一致的情况,导致缓存失效策略异常。而Cache Control存放的是过期时间,以秒为单位,不会存在缓存策略失效的问题。
当我们在开发中总写一些重复的代码时(比如总是判断用户是否登录等等),就应该想到拦截器、过滤器或AOP,其实他们都是面向切面变成**的具体体现。比如因为这个方法需要判断user是否为null,如果为空则返回登录异常信息给用户,判断用户是否未登录的逻辑非常普遍且统一,所以我们可以自定义一个@NeedLogin注解,然后在拦截器、过滤器、AOP中判断方法是否有该自定义注解@NeedLogin,如果有的话在拦截器、过滤器、AOP中直接进行判断,如果未登录,阻止controller后续方法执行,并返回用户登录异常的信息。
假设有这样的业务场景,一个用户只能操作一次数据(比如秒杀),这样的情况下,要防止用户快速点击大并发下,导致用户实际上能操作两次(秒杀同一商品两件),这种情况的发生,我们就可以利用唯一索引实现,因为在下单的过程中会修改(update)某条数据,在mysql的innoDB引擎下会对这条数据加上行级排它锁,加排它锁后相当于对这条记录的修改就是串行化的,所以是无法插入被唯一索引修饰的,从而在高并发下实现防止同一人买两次的情况。
核心还是减少数据库的访问
- 系统初始化的时候将商品库存信息加载到Redis中
- 在spring初始化bean的时候,如果该bean是实现了InitializingBean接口,并且同时在配置文件中指定了init-method,系统则是先调用afterPropertiesSet方法,然后在调用init-method中指定的方法。
- Spring为bean提供了两种初始化bean的方式,实现InitializingBean接口,实现afterPropertiesSet方法,或者在配置文件中通过init-method指定,两种方式可以同时使用。
- 实现InitializingBean接口是直接调用afterPropertiesSet方法,比通过反射调用init-method指定的方法效率要高一点,但是init-method方式消除了对spring的依赖。
- 如果调用afterPropertiesSet方法时出错,则不调用init-method指定的方法。
- 收到请求,Redis预减库存,库存不足,直接返回秒杀失败,否则进入第三步
- 请求入队,立即返回排队中
- 请求出队,生成订单,减少库存,成功后将秒杀信息记入Redis
- 客户端轮询,是否秒杀成功(可以通过ajax轮询,因为轮询时,回话并未结束,我们可以回获取到用户信息,做一些事情,轮询接口只是对Redis进行查询,所以效率非常高)
- 客户端通过js轮询的时候,不可避免的会递归调用,但是递归条件是客户端根据返回值判断如果还在排队中再进行轮询。如果排队结束,则根据返回值跳转或提示不同的内容。
以上修改主要有两大好出,降低数据库的使用,下单流程是没有阻塞的!以上其实就是用队列将请求与真正的响应做出隔离,通过队列的特性,让DB可以安全,平稳的抗住并发。就像是买票一样,我们去买票就相当于一个请求,售票窗口就相当于服务器(对我们的买票请求做出响应),如果不排队,所有人都挤在一起,并发的买票,那很可能门都挤破了(参考大学挤破图书馆事件),所以加入队列后,售票窗口就能平稳的工作,不会发生挤破售票窗口的事件了。
我们以前玩游戏的时候都会发现如果游戏突然间很卡,大概率操作系统会直接将程序杀死,这样既机制是操作系统防止因为软件让自己崩溃。服务器也是运行在操作系统上的,如果mysql并发太高,负载太大,操作系统为了自己的安全依旧会杀死mysql服务,这就是如果我们不优化,在高并发场景下mysql最终的归途,当然在崩溃之前肯定会长时间的处于无法响应阶段,对于长时间无法响应的程序操作系统为了防止自己被拖垮,会自动杀死。
一般情况下,我们在下单的时候总会经历在同一个事务中操作如下步骤:
- 判断库存,是否大于0
- 如果大于0则下单成功,减少库存
- 生成订单表
以上判断方法需要在查询商品的时候给它加上排它锁,这样能保证数据的绝对一致,因为查询操作都是一个串行化操作了!但是性能上可能就会有更大的开销,所以可以优化如下
- 减少库存(update语句),但是加上限定条件库存>0
- 如果上一步中操作数据库返回的cuount不是0的话,代表着减库存成功,那么就可以下单,如果上一步减库存的count是0,那就说明减库存失败。
以上优化的好出是,不用手动上排它锁,update的时候自动对数据库进行行级排它锁。减少上锁的次数,优化代码执行。
我们都已在一分钟内限流为例
一般情思路,我们下意识的会想到
- 当请求来的时候记录ip,与当前ip的累积访问次数
- 如果该ip超过当前分钟的访问次数限制,那就阻止期访问对应的次数
- 开启定时任务,对ip的访问次数周期性归零或清除操作
- 开启定时任务,对被加入访问限制的名单进行周期性解除
以上思路的弊端就是很麻烦,不好理解容易出bug,并且逻辑设计不合理的话,很有可能会产生大量定时任务,拖慢整个系统的运行,系统的吞吐量就会降低。
第二种思路
我们通过缓存来实现限流的逻辑
- 当用户请求一个需要限流防刷的接口时,在redis中以用户ip为key,次数为值,然后设置该K-V键值对的有效时间为1分钟
- 当用户再次请求的时候,判断该ip(或是用户id),是否在Redis有值(xxxNX方法),如果有值的话判断是否已经到达限流红线或是限制访问了
- 如果用户已经到达限流标准的话,直接将该K-V键值对的值设置为X(代表已经拉入黑名单),XX(一级限流),XXX(二级限流)等等,根据限流的等级再将该K-V键值对设置对应的失效时间即可
在日常的开发中,如果避免DB的操作呢?很简单,就是把缓存当做一个中间层
- 客户端想访问DB的信息时,先去访问缓存
- DB操作完毕后,第一时间将客户端需要的结果放在缓存中
以上的模式就可以巧妙的避过大量DB读操作。
主要是一些开发三层架构的编码规范以及思路,纲领就是每一层都要有每一层所做的设计**。
- dao层查出的对象是DataObject,这种对象与数据库是一一对应的,也就是说数据库表有什么字段,那么对应的DataObject就有多少个字段。这个对象是一个最最简单的一个orm的映射。如果一个类名的结尾是DO,那就说明这是一个DataObject。
- service层绝对不能简单的将dao层查出来的DataObject返回给前端用户,在service层中应该有一个model的,这个model才是真正上springmvc业务逻辑交互模型中model的概念。model才是我们处理业务逻辑的一个核心模型,而DataObject仅仅是对数据库的映射。为什么这样设计呢?因为在我们设计表的时候,可能为了性能或扩展性将用户信息存在两张表中,这样关于用户的DataObject就会有两个,但是用户model应该只有一个,因为一个领域只需要一个用户对象。简单的说,model是由DataObject组装来的,只不过绝大部分情况都是一个DataObject刚好对应一个model。
- model就是一个领域模型,用于处理我们的业务逻辑。领域模型是不能直接返回给用户的,前端需要拿到最小知道的对象进行处理,即VO对象。所以在开发中我们需要将领域模型model组装、转换成可供UI层使用的VO对象。
首先在客户端的体验上,异步servlet与同步servlet相同的业务逻辑处理的时间是相同的。比如业务逻辑需要5s处理完成,那么客户端无论是同步servlet还是异步servlet都需要等待5s。
他们的区别在于对tomcat线程的占用。同步servlet会阻塞servlet的线程,一个请求没有处理完成时,会一直阻塞这个线程,知道请求处理完成,这个线程才会被释放。异步servlet会将耗时操作放在另一个线程中执行,当这个线程执行完毕后再通知接收到请求的servlet线程,完成相应,这样做的好出是,当servlet线程收到耗时的阻塞请求时,会立刻将阻塞请求放在异步线程中处理,自己的就可以继续接受别的请求了,这种非阻塞模型会提升并发性能,因为以前阻塞模型时,有多少个请求就需要开启多少个线程,现在异步servlet的话可以一次接受很多请求,然后由异步线程慢慢处理,从而实现提升并发的效果。
同步servlet阻塞的是tomcat容器的servlet线程。
以上异步servlet就可以做到使用较少的线程,达到较高的吞吐量。这里的同步servlet与异步servlet都是服务器端的概念,客户端的体验肯定是阻塞的,因为一次请求对应个相应就确定了客户端肯定是阻塞体验!(说句题外的,就算将请求放在消息队列,然后快速返回客户端处理中,让客户端轮询访问另一个接口获取结果的方式,依旧是一种阻塞体验,因为没有脱离一次请求返回一次相应结果的模型,如果能做到一次请求多次相应,那就不是阻塞体验了!)
- 这里主要是spring框架相关知识(如aop 如何在springboot上使用等)
一直被这几个概念搞糊涂,现在尝试着理解一下
首先可以这么说吧
根据有道的翻译,我们可以先知道,上下文与环境其实是一个意思。所以我们的spring上下文与spring运行环境也是同一个东西。
当我们看到context结尾的类时直接想到运行环境就没错了。
我们为什么在使用大部分框架的时候都需要初始化运行环境(上下文)。
举一个栗子吧。
比如我现在租的小房子就是我的生活环境,说成是我生活的运行环境也肯定是没错的。我洗脸需要洗面奶,那我就从我的生活环境中拿到洗面奶就可以了。我想要睡觉那就在生活环境中获取到床,然后我调用自己的睡觉方法,把床作为参数传递进去就可以了。简单地说,我在生活中需要的各种东西,从我的生活环境房子中去取得就行了。换一个更加程序的说法就是,我在运行的时候,需要的一些对象,直接从我生活的运行环境中取得就可以了。
听过上面的例子,那我们对spring运行环境或者上下文就能有一个较为清晰的理解了。spring的应用环境作用与我们的生活环境作用没有什么区别。spring在运行的过程中肯定需要使用一些对象来完成很多事情。那spring从哪些地方获取需要的对象呢?就是spring自己的上下文(运行环境)中取得。所以我们对spring上下文或者spring运行环境理解成支撑spring正常运行的各种对象的集合也就没错了。就像我的上下文,其实就是电脑,牙刷,手机,床,空气等等各种对象的组合。如果上下文没能初始好,那么无论是我还是spring都会运行不下去的。所以初始化上下文是非常非常重要的一个环节。
在学习ssm与springboot中,我们有一个明显的感受就是,web.xml文件的消失。在原来的方法中web.xml在启动时,被tomcat读取,并根据相关配置逐行执行。所以我们将spring.xml文件配置到web.xml文件中,就可以随着服务器的启动,而创建出一个spring的ioc容器,从而实现在项目中使用spring的目的。
但是想想,加载spring框架的配置文件的方式只有这一种吗?
显然不是的我们可以不依赖web容器的启动,通过main方法以及spring提供的new ClassPathXmlApplication("类路径上的配置文件名称")
从而创建一个spring的核心容器。或者通过main方法加载注解类创建核心容器。以上三种方式,本质上都是,创建一个核心容器,该容器里面存有配置文件配置的相关bean。这些在ioc容器中的bean就将生命周期交给了spring去管理。
在springboot中加载spring配置文件的方式,已经被透明化了,但是我们最起码要知道,springboot帮我们完成了配置文件加载这件事(而不能说,springboot没有加载配置文件!),springboot通过包扫描的方式加载配置文件,我们直观的看不到加载配置文件的代码。
可以反过来想想,为什么创建IOC容器一定要加载一个对应的配置文件才行。如果不加载配置文件的话,IOC容器中没有业务bean,如果没有业务组件bean的话,那么就无法使用自动注入功能,无法通过aop对业务组件bean进行横向增强。这样的IOC容器是毫无意义的容器,所以一定需要加载配置文件,其实就是将业务组件bean加载到IOC容器中,从而使用spring为我们带来的便利!!!
在想要用的时候不用自己new了,如果有修改,直接把配置文件(@Configuration、xml)一修改,不用像老黄牛一样修改每个类出现的地方!!!别看这个问题不大,但是如果你的框架中你自己new了500个类了,然后觉得这个类的构造方法需要改变,如果不是通过依赖注入的方式,那么这个项目基本上是可以放弃了,修改这500个对象因为构造方法的改变带来的错误,就已经让人崩溃了。
其实依赖注入更多的优势是简化框架开发者的开发难度,如果我们在自己的项目中只是使用现成的框架,还真不能感受到依赖注入有多么牛逼。最起码起来注入让我们完全不用搞懂工厂模式了,当然还有很多特点!业界流传的一句话spring让java设计模式消失,可不是一句空话。
首先测试代码为:可以看到两个接口的区别就在于是否获取session,也是为了从侧边印证tomcat创建session的时机在于request.getSession()方法被调用的时候。(访问jsp页面的情况不考虑)
开始测试:
首先清除本域下的cookie,因为sessionid就存在本域下的cookie中
发送请求后我们看到服务端有如下接收显然这种方式下,springmvc已经通过request.getSession()方法自动帮我们创建一个session空间。如果没有理解错的话,我们将会在本次请求的响应头中看到set-cookie字段,内容就是jsessionid=3B06A4206......D868。我们把断点放开,看http报文
放开后
跟我们预想的是一致的,当我们再次发送http请求时,应该要带上jsessionid=3B06A4206......D868,如下,简短的刷新页面后我们看到
http报文为
这样就通过cookie找到对应的session,从而实现保持回话状态的功能,可以预想到的是,不管客户端的cookie丢失(过期)还是服务端的session空间丢失(过期),都会导致回话失效,用户需要重新登录进行认证。
接下来我们看看springmvc中不获取HttpSession是否会自动帮我们创建一个session空间,首先还是清除cookie后向服务端发送请求可以看到本次请求是干净的,没有任何cookie信息被携带,然后就是我们的重头戏服务端放断点,看看是否会在响应中有set-cookie字段。结果如下
并没有set-cookie字段,也就说明了,springmvc并没有自动的调用request.getSession()方法,只有我们需要session时,才会调用这个方法,返回或创建一个新的session空间。
总结:只要将session或cookie随便失效一个,那么会话状态都会丢失。这也是我们登出用户的思路,标准的情况是让服务端的session直接失效(删除当前session空间),从而实现用户登出的效果。在快乐慕商城中的登出做法如下可以看到只是将当前session空间中保存的用户信息进行删除,从而使会话失效,这样做session空间还是存在的,剩下的就是过30分钟让session失效,这样做的好出有防止session的删除与创建,能节省创建时的系统开支,比如用户登出后,换另一个账户登录,session空间还是一开始获取的那个,只不过保存的内容变了,变成新账号的用户信息了!
- springboot相关配置(比如对mvc的配置,对拦截器的配置等)
- springboot源码(比如自动装配原理)
- 一些与springboot集成的方法(比如springboot日志框架的替换或使用)
Spring Boot将功能场景抽取出来,做成一个个的starters(启动器),只需要在项目里面引入这些starter 相关场景的所有依赖都会导入进来。要用什么功能就导入什么场景的启动器
首先我们需要读懂这句话
摘抄1:
编译不通过不代表不能运行其字节码。直接跑class文件的时候,如果引用到了其他类,那么类加载器会尝试加载该类,如果在classpath中找不到对应类的class文件,就会报ClassNotFound异常。另外,除了显式的import其他类,还可以通过Class.forName方式加载其他类,这样就可以解决编译不通过的问题了。举个例子的话就是java中sql driver的加载方式: Class.forName("com.mysql.jdbc.Driver");。
摘抄2:
首先这些@Configuration类没有被程序中的类引用到
其次即使引用到这个类,不一定引用到类中的具体某个方法。 查看一下spring类加载器的原码??
虽然这些地方import失败了, 但是不影响.class类加载,
也就是说编译这些@Configuration类时依赖的jar是必须存在的,但是运行时这些jar可以不提供
类加载的时机:创建该类的实例对象,或者引用了静态方法
以上我们就能理解到,如果在编译的时候将maven的scope设置为support级别,那就会导致编译的时候,有某个jar包,而运行的时候并没有该jar包。如果程序在线上一直没有用到该jar包的话,那是可以正常运行的,如果用到了该jar包,就才会报ClassNotFound异常。也就是说如果不进行类加载(创建该类的实例对象,或者引用了静态方法),即使有没有引入的jar包,也不会抛出ClassNotFound这个异常,程序也能正常的执行。以上就为@ConditionalOnClass的现象做出了解释。现在再看看这中源码,是不是就很舒服了!因为飘红的就没有被加载,没被加载程序也就不会使用,所以代码可以完美执行。如果想要用@ConditionOnClass之类的注解,就需要保证在编译的时候,类路径是有该类的,执行的时候有没有无所谓!
还是为了两点:
- 自动配置(让我们少配置写bean),这点是尤为重要啊,以前引入一个框架至少需要将框架的某个类加入到核心容器中,加入核心容器的时候,无可避免的要设置很多属性,但是如果使用起步依赖的话,当我们引入starter的时候,就会直接引入相应的bean,我们在使用的时候,直接通过@Autowride就可以在我们的业务中使用框架或类库带给我们的便利了,是不是超爽啊,而且修改配置,因为starter是通过@EnableConfigurationProperties()加载配置类的,因为配置类标有@ConfigurationProperties(prefix="xxx"),所以我们可以在application.yml文件中直接对这个框架进行配置上的修改。
- 起步依赖(简化maven的坐标依赖,防止包的版本错乱)
回想起我们自己配置ProcessEngine的场景,如果能引入一个起步依赖然后通过@Autowired直接注入一个合适的ProcessEngine(或各种Service)岂不美哉,又能减少配置,又能防止重复配置可能产生的人为错误,何乐不为呢!
题外话,如果不想魔改activiti的启动器,又想在springboot项目中使用,其实完全可以参考shiro在springboot项目中的使用方式,直接把xml修改成java类的配置方式,然后该注入的注入,该调用的调用就行了。
不使用启动器,肯定也是可以在springboot项目中使用的。只要想办法把activiti中的关键类加入到核心容器中,通过spring管理这些类,完成功能增强(aop、事务等),或是依赖注入就可以了。这样整与springboot起步依赖相比大约有以下缺点:
- 依赖是我们自己管理的,依赖本身的版本号,或者依赖内部依赖的别的版本号等,我们引入相当于是一个jar包而不会想springboot一样是一个功能。
- 肯定也是不能直接通过application.yml对我们框架进行设置了(通过属性读取器是可以,但是肯定不方便)
我们不要陷入一个误区,好像一个菜单下面的子项都是同一个微服务,其实微服务是按照模块拆分的,肯定是拆分的越细致越好,所以出现一个菜单下的功能分属于不同微服务很常见,同样的一个页面中的数据,分属于不同的微服务也很常见,要时刻牢记以上两点。
一般而言拆分思路是按照领域组件进行拆分,但是领域组件具体是什么是由领域专家来决定的。将一个个领域组件拆分为微服务的好出是,能实现类似于单一职责与最少知识原则的好出,可以按照领域组件水平扩展微服务。
首先我们需要明确,微服务是一个架构**,而不是具体逻辑的框架实现,微服务最终是通过分布式开发来实现微服务的架构**的。
- sql语法
- 调优
- 使用技巧
- 锁的使用
定义类别表->定义表->定义对应实体表
在数据库设计中,切记一层一层的寻找对应关系
什么意思呢?
就比如打分定义表与报告表的关系
一个报告包含多个打分指标(非打分指标定义),一个打分指标对应一个分数(一一对应的就可以是一张表)!
而一个打分指标定义对应多个打分指标,此时打分指标这个表就相当于被两个表一对多关联了,所以自然而然的就成了这两个表的中间表!
"xx定义表"的价值就像是一个类,而定义表对应的"实例化表"就相当于一个对象,"xx定义表"的意义在于可以将"实例化表"的一些公有属性进行抽象,并实现配置化的功能。想要配置化,一定要引入定义表。
总结,像是每个页面都会都会出现的内容,其实都可以抽象一个定义表,然后配置化相关内容。甚至是每个循环列表项,每个下拉菜单,都有抽象成一个定义表的可能!
定义表是无状态的,不会存某个用户特有的数据,定义表对应的实例化表才是存特定数据的地方。一般而言,定义表是一对多的一!
像是考卷的试题,就可以有试题定义表,试题定义表的实例化表,就会存,张三的某个试题得了多少分!如果试题业务场景每人只用回答一道题,那大体的设计思路,也是这样的!
返过来想一想,如果没有定义表,就以试题为例,业务场景中每个用户需要回答多个试题。
如果我们不存在试题定义表,那么存储的的形式符合三大范式吗?
以张三为例,张三需要存储每个题都需要存储标题等信息,而大量的重复这些不变的信息,我们就需要警觉了。这样的设计就不符合第二范式了。非主属性完全依赖于主键。
我们可以参考后边这个文件,让我们对三范式有更加直观的了解[第一、第二、第三范式之间的理解和比较 - 涛声依旧_ - 博客园.pdf](/page/第一、第二、第三范式之间的理解和比较 - 涛声依旧_ - 博客园.pdf)
自己做的Excel中,其实应该两个领域对象,两个领域对象的分析,先分析用户题目表,因为用户去做题,然后才会有得分,分析的思路是将用户与题目表该有的所有元素写在同一张表中,然后套3大范式,发现不符合第二范式,修正后形成中间表。
然后,根据业务分析,得分与用于和题目都有关系,所以直接扔到一个与用户和题目都有关系的那张中间表就行了!很简单吧
模式就是关联两两进行设计(两个表所有信息合在一起就行),然后套3范式,主要是1与2范式,主要是第二范式,消除联合主键,遗忘了就想想本次评价的相关设计套路!
以本次的上传与打分定义为例子。首先一个报告包含多个附件定义,所以将这两个表的字段写在一起,多写两条数据后就会发现联合主键的问题,需要一个中间表。同样的一个报告也包含多个打分定义,依旧存在联合主键的问题,需要中间表,消除联合主键。
最后就是打分定义表与附件上传表,其实根据项目经理提供的"工作底稿"我们将这两个表的字段放在一起后发现会有联合主键的问题,所以依旧需要消除联合主键,引入第三张表。
最后就是打分的分支问题,根据业务场景,分值与报告和打分定义都有关系,毕竟一个确定的分支是由一个确定的报告和报告中某一个确定的打分定义项来的,所以既然都有关系,肯定也是存在中间表中,刚好有一个报告打分中间表,所以自然而然的就存在了"报告-打分定义中间表"中了。
上面我们学会了,去设计多对多的表,那如果甄选一对多呢?
很简单还是写在一起,如果发现不同的主键对应相同的某个,或某几个数据列,而这几个数据列的主键是相同的,那就是一对多的关系。
一定不要再命名成 r_id 之类的字段了!!!太坑了,因为自动生成domain的时候,会因为生成setter方法与getter方法导致入库出现问题!!!而且很难排错,所以解决方案为,不写再图省事写x_id这样的代码了,不光是主键字段!
在mysql中有很多保留字段,比如desc,一点字段叫作desc就会报sql异常的错误,大体上错误如下:
'desc,xx,xx)values(?,?,?)'
根据最近的观察,数据库所有表的主键名固定为id,非常合理,编写代码的时候会处理起来思路会很清晰!
- 一定要看报错信息,主要看最后一些报错!因为这几次出问题都是看最后的报错信息才改正的。
- 在domain中设置集合的时候,一定不要用集合框架的实现类进行接收,需要用List或Set进行接收
曲江项目就存在这样一个问题。
我们的报告表本来是存储"曲江评价报告"的,但是业务后期突然让加了一个"公共信用等级评价",这个数据最终因为查询列表的问题,也放在了报告表中
如此一来,我们就需要一个type字段来区分这两张表,等到项目上线的时候,发生了一个意想不到的问题。那就是我们查询"曲江报告用的是字段1",但是老数据的曲江报告type字段为null(毕竟以前没有这个字段么)。这就导致老数据查询出现问题,幸亏数据量不大,把type为null的字段通过where全部修改了!
但是也为我涨了波教训,以后在设计表的时候,有可能一张表存两种相关的数据时,一定要在一开始就加入type字段,从而保证面对未来的扩展也留有一定的能力!
本次不谈语法,只谈在关系型数据库中,连表查询的注意事项
我们在关联查询的时候,关联条件一般是 a.主键 = b.外键或反过来。
切记注意不要观察两张表,有一样的字段就直接进行关联查询,因为很有可能这两张表有关联的字段其实根本不能作为多表查询的条件。如果关联条件不正确一定不能查询到我们想要的结果,最好的情况查询结果是null,要不然查询出来的就是脏数据!
在严格遵守以上条件的情况下,如果存在a.非主键但唯一的字段 = b.主键,或反过来。
这种情况其实就是一对一的查询
如果是a.非主键但唯一的字段 = b.非主键但唯一的字段,且这两个字段是代表同一个东西,比如流程引擎中的businessKey与业务表中的主键,这样子也能作为关联条件进行查询,其实还是一对一,只不过没有明确外键主键关联关系。
根据颗粒度数据库锁可以分为行锁与表锁。根据锁的性质可以分为共享锁与排它锁。在执行update、delete、insert的时候会给结果集加上排它锁。select默认是非阻塞的锁,我们也可以手动为select加上共享锁或排它锁。共享锁不会阻塞共享锁,但是会阻止排它锁的执行(无法执行成功)。而排它锁会**阻塞(block)**共享锁与排它锁的执行(获取锁之后会继续自动执行)。
根据测试,在同一个事务中,不会出现锁的情况。比如:
- 首先保证事务的自动提交关闭
- 执行select语句对当前结果集进行加锁
- 然后对id=126的数据进行更新
- 发现更新成功,再次查找也是新的数据,以上说明,对某一条结果集加锁后,不会阻塞同一个事务中的排它锁执行,但是会**阻塞(block)**别的事务的排它锁执行。
在当前章节案例中,session01是当前会话。session02是其他会话。
首先可以确定的说,在innodb引擎中,索引使用正确的时候,会进行行锁。
我们开启两个会话
我们将事务修改为手动提交
然后在session01中修改某条数据,在session02中同样对这条数据进行修改,我们可以发现,session02中的修改被阻塞了。相当于形成了行锁(innodb与正确使用索引时)。session02的日志如下耗时40多秒,很明显被阻塞执行了,否则这么小的数据量,没有网络影响的情况下,不可能执行这么久!
如果索引使用不当,会导致行锁变表锁的!
我们现在需要保证查询某项数据时,将结果集锁住,防止因为并发出现的超卖现象,做法为:在session01中执行
select * from mmall_cart where id=126 for update
,然后在session02中执行所以根据上面的现象,我们可以得知,阻塞只发生在,都在竞争某一个相同的锁时,才会阻塞,等待获取锁。如果语句无需锁就能执行,那就不会被阻塞!就像session02中
SELECT * FROM mmall_cart WHERE id=126
的执行无需锁,所以即使session01中使用的排它锁,锁住了id=126的结果集,在session02中依旧可以直接查询到当前结果集(已提交的事务,没有脏读现象)。因为SELECT * FROM mmall_cart WHERE id=126
压根就不会去竞争锁,所以不会被阻塞执行!!!这点的坑也是想了很久才爬出来的!!!
在mysql的事务中,是通过锁机制来保证不出现脏数据的。具体的做法是,根据事务中的语句,分别对每条语句的结果集加上对应的锁。锁的目的就是为了控制并发访问,使得并发请求变为串行化的请求模式。所以在数据库加锁之后,别的会话就会被阻塞访问,只有获取锁之后,阻塞情况才会消失,会话才会返回对应的结果集。
在传统的业务中,我们一旦看到关系型数据库第一反应一定是,所有数据之间都是有关联的,扩展关系并不会影响原有数据的关联关系,只是新增了一种关联关系。熟知这一点我们就可以放心大胆的扩展业务。
关系型数据(关系型数据库),最大的特点还有一个就是关系会传递,比如A与B有外键关联,B与C有外键关联,C与D有外键关联,那么A表中的某一条数据,绝对可以根据关联关系找到D表中对应的某一条或某几条数据。
其实数据库函数很简单,就是在解析sql的时候,如果有数据库函数,会将数据库函数以及他的参数解析成一个标准的字符串,然后再进行对比,例如:date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'
date_format函数就是将数据库中的日期转化成xxxx-xx-xx的形式,然后再与我们传入的2008-08-08进行比较,根据这个条件返回结果。
要记得数据库函数在最后一定是转换为标准模式进行最终解析的。
- 生成器使用
- 框架功能(比如如果反写主键)
在mybatis的xml文件中,会有这样的属性总体而言parameterType的value是java的一些内置简单对象,比如map,string等等(小写的map只是一个缩写,可以写为java.util.Map)。而parameterMap是我们在mybatis的xml中定义的一些对象(一般而言是entity对象),如下:
- redis主从配置
- redis使用技巧
- redis功能方向
- redis使用**
首先我们需要知道什么事hash算法
Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。 详见目录下/page/常见的hash算法及其原理 - Beyond_2016的博客 - CSDN博客
首先我们应该清楚redis的分布式算法,肯定是建立在对将要存入redis的key进行算法,然后映射到一个对应的redis服务器上,这样子我们在取值时,才能根据要取出的key,找到对应的那个redis服务器,然后对这个服务器中保存的数据进行读操作。
最原始的分布式redis算法原理如下:
假设我们要存储20条数据,根据上述的redis分布式算法,数据存储方式如下:
以上存储时,如果需要添加一个redis节点,那么会出现如下情况
之前保存的20条数据会因为加一台服务器后,失效16个,这16条数据就需要重新读取写到缓存,所以以上redis分布式存储带来的问题就是,在添加节点时,会导致缓存的命中率骤降,发生缓存穿透的现象,如果并发高,很可能会导致服务器崩溃,但是添加缓存又经常发生在我们需要大规模推广的时期。所以有了consistent hash算法来保证分布式redis不会出现,因为增加一个节点,导致缓存命中率骤降的情况。
一下为consistent hash算法的示意图,我们可以看到所有的计算都是基于一个环形hash空间的。
Cache在环形空间的落点一般是通过机器的ip地址或是主机名或等等,计算出来的。
但是上述算法还有一个问题需要解决
以上两张示意图就展示的这种问题,ABC代表三台redis服务器。
所以要解决hash倾斜性,导致负载不均衡的情况,思路如下
虚拟节点的数量越多,数据负载就越均匀,但是如果虚拟节点太多性能就不够好,所以真是节点与虚拟节点的比例是需要我们根据真实情况控制的。一般情况我们会给一个真是节点配置100-500的虚拟节点。
我们变动的服务器台数为m。
以上consistent hash算法已经最大程度的减少了,服务器增减时的缓存重新分布。
注意一点上面的consistent hash算法并不是仅仅适用于redis,而是一个经典的分布式算。
相关概念:
- 多个集合构成一个数据库
- 一个MongoDB实例可以承载多个数据库,与mysql相同
- 每个数据库拥有独立的权限
- 每个数据库放在不同的文件中,最终会成为文件系统中的文件
- 命名规范
- 不能带有""字符串
- 不得含有空格、.、$、"、"、"/"、""和空字符"\0"
- 全小写
- 最多64字节
- 文件名不支持的数据库名就不支持
- 不能使用保留数据库名
- admin
- local
- config
mongo的document,类似于关系型数据库中的行,值得注意的一点是,文档是有序的,不同顺序的文档,其实不能化分为同一类文档。
mongo可以将键不同,值类型不同的文档放在一个集合中,但是为了编程和管理方便、数据库处理速度,建议把不同类的document放到不同的collection中。
在mongo中,我们描述多对多等关系的时候,明显是不符合关系型数据库的第一范式的,即字段不可再分原则。在关系型数据库中,我们描述多对多关系的时候会有一张中间表,但是在mongo中,我们描述多对多关系的时候,就是在一个ids字段中,存入多个id(数组形式存储)。这显示不符合第一范式,但是在非关系型数据库数据库中就该这么玩。
问题:
数据库设计中,数据之间的引用不可避免,其中常见的模式就是一对多。举个例子:
Person和Addresses
Person是一个对象,地址是一个对象,一个Person可以有若干个地址。
方案:
原文作者提到了三种设计方案,以及设计原则:
1,One-to-Few(一对有限数量的多)
例如上例中的Person和Address,一个Person基本只有少数量的居住地址,并且在很大程度上,没有单独读取Address对象的需求(也就是说是否有必要单独建立一个adress的表格),这种情况下采用嵌入式模式(embedding):
2,One-to-Many(一对千量级的多)
例如Product(产品)和Part(配件),一个产品,例如汽车,可以有数百个配件,而且配件通用性,也导致有需求单独对于配件进行查询,所以考虑到数量和应用,这种情况下适合采用child reference(在Product中添加引用数组)
3,One-to-Squillions(一对无数)
例如分布式系统的日志,每个机器(Host)会产生大量的日志记录(record),这种情况不适合child reference,也就是说,引用数组的长度无法控制,很可能越界,所以这种情况下适合采用parent reference(在每条record中,添加一个反向引用,指向Host)
我们在更新mongo的时候,要防止进行了文档替换操作,需要通过$set进行操作,设置想要更新的字段,这里只是点出来注意点,具体操作可以去看官方文档。
java代码想要与MongoDB数据库进行交互,也是需要通过MongoDB官方提供的数据库驱动来完成,数据库驱动中就包含了,能够操作MongoDB的各种类。
在springboot中,当且仅当我们引入了mongo的启动器的时候,就可以自动注入一个MongoTemplate 的 bean了。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
@Autowired private MongoTemplate mongoTemplate;
在使用MongoTemplate的使用中,有两个类需要我们特别记忆
- Criteria类:它封装所有的语句,以方法的形式进行查询。
- Query类:这是将语句进行封装或者添加排序之类的操作。
简单的说就是通过Criteria构造查询条件,然后封装成一个Query类(一般是通过Query的构造函数进行封装),最终将封装后的Query实例传入到MongoTemplate的增删改查方法中进行查询。
首先有这样的现象加入@DBref之后在入库时的表现为
我们可以看到在当前文档中只是保存了另一个文档的id
已知的有
@DBRef private Shop product;
@DBRef private List accounts;
如果不加@DBRef,就保存整个Shop文档信息,如果加上了,只保存Shop文档的id。
比如商品文档里面如果保存了整个商家表的信息,商品文档内容太多了,应该只保存商家文档的id。
如果加上了@DBRef,保存商品表不会保存商家表,商家表文档应该先单独保存,然后保存商品表。
其实一开始有点看不懂这个注解是因为,我们惯性思维下,认为@DBref注解是用来描述数据库的,其实我们应该认为@DBref是用来在插入数据是影响数据库的。(描述数据库与影响数据库是完全不同的思路),因为我们总是站在查询的角度去想ORM对象,这点应该要加以注意。**如果站在查询的角度上,无论是否加了@DBref都是没有任何变化的,这就是一开始不理解这个注解作用的原因!**一个思路想不通的时候,不妨站在另一个角度去想问题,没准会有惊喜嘞。
同样的@Indexed注解依旧是用来影响数据库的而不是用来描述数据库的!
主要是记录一些技术框架使用思路,比如POI,activiti,这个章节的特点就是都是技能型知识,第一要求就是看到自制文档后能快速上手使用。并且代码比较固定,需要我们强行背下来,是记忆为主理解为辅的章节。
红星的经历让我明白了,技术真的是为业务服务的,也只能为业务服务!因为业务比技术要千奇百怪太多,而且没什么道理可言!但是红星的经历又告诉我,看似高大上的微服务,绝对有公司严格落地的,落地成本并不高,所以无论身处何处要积极的去学习新技术或核心技术,因为无论业务如何改变,所用的技术都是按照标准来的,不标准的技术根本没有学习的意义。因为在红星的业务场景下,POI、rabbitmq、redis其实也就是常规的用法,所以提前学习核心技术走到哪里都不怕!!!
在企业级应用开发中,Excel报表是一种最常见的报表需求。Excel报表开发一般分为两种形式:
- 为了方便操作,基于Excel的报表批量上传数据。
- 通过java代码生成Excel报表。
POI导入坐标时需要注意,我们是按照我们所要处理的场景去导入坐标,具体的导入规则如下:
Apache POI分布包括对许多文档文件格式的支持。该支持在多个Jar文件中提供。并不是所有的jar包都是需要的。下表显示了POI组件、Maven存储库标记和项目Jar文件之间的关系。
Component | Application type | Maven artifactId | Notes |
---|---|---|---|
POIFS | OLE2 Filesystem | poi | Required to work with OLE2 / POIFS based files |
HPSF | OLE2 Property Sets | poi | |
HSSF | Excel XLS | poi | For HSSF only, if common SS is needed see below |
HSLF | PowerPoint PPT | poi-scratchpad | |
HWPF | Word DOC | poi-scratchpad | |
HDGF | Visio VSD | poi-scratchpad | |
HPBF | Publisher PUB | poi-scratchpad | |
HSMF | Outlook MSG | poi-scratchpad | |
DDF | Escher common drawings | poi | |
HWMF | WMF drawings | poi-scratchpad | |
OpenXML4J | OOXML | poi-ooxml plus either poi-ooxml-schemas or ooxml-schemas and ooxml-security | See notes below for differences between these options |
XSSF | Excel XLSX | poi-ooxml | |
XSLF | PowerPoint PPTX | poi-ooxml | |
XWPF | Word DOCX | poi-ooxml | |
XDGF | Visio VSDX | poi-ooxml | |
Common SL | PowerPoint PPT and PPTX | poi-scratchpad and poi-ooxml | SL code is in the core POI jar, but implementations are in poi-scratchpad and poi-ooxml. |
Common SS | Excel XLS and XLSX | poi-ooxml | WorkbookFactory and friends all require poi-ooxml, not just core poi |
此表显示jar依赖关系。“version - yyymmdd”是POI版本。你可以在 downloads page. 看到最新信息;
Maven artifactId | Prerequisites | JAR |
---|---|---|
poi | commons-logging, commons-codec, commons-collections, log4j | poi-version-yyyymmdd.jar |
poi-scratchpad | poi | poi-scratchpad-version-yyyymmdd.jar |
poi-ooxml | poi, poi-ooxml-schemas | poi-ooxml-version-yyyymmdd.jar |
poi-ooxml-schemas | xmlbeans | poi-ooxml-schemas-version-yyyymmdd.jar |
poi-examples | poi, poi-scratchpad, poi-ooxml | poi-examples-version-yyyymmdd.jar |
ooxml-schemas | xmlbeans | ooxml-schemas-1.3.jar |
ooxml-security | xmlbeans For signing: bcpkix-jdk15on, bcprov-jdk15on, xmlsec, slf4j-api | ooxml-security-1.1.jar |
从上面表格中可以看出,如果只是处理Excel,我们只需导入poi,poi-ooxml, poi-ooxml-schemas就可;
主要是观察文件后缀的区别
- HSSF提供读写Microsoft Excel XLS格式档案的功能。
- XSSF提供读写Microsoft Excel OOXML XLSX格式档案的功能。
- HWPF提供读写Microsoft Word DOC格式档案的功能。
- HSLF提供读写Microsoft PowerPoint格式档案的功能。
- HDGF提供读Microsoft Visio格式档案的功能。 HPBF提供读Microsoft
- Publisher格式档案的功能。
- HSMF提供读Microsoft Outlook格式档案的功能
Workbook的子类有,我们可以根据上边列表的区别进行实例化。
一般情况下我们需要处理的就是XLSX,即Excel2007版本。
我们需要牢记一点,使用POI生成或读取EXCEL是做法跟我们手动写入或读取EXCEL的步骤是一样的!**没错就是一样的!**使用POI的时候我们也需要跟肉眼读取一样。
我们手动操作excel的不走是,首先创建一个报表,然后创建一个sheet,然后创建一个行(隐式),然后再在各自中写内容。
- Workbook---Excel的文档对象,即代表整个Excel文档文件,针对不同的Excel类型分为:HSSFWorkbook(2003)和 XSSFWorkbool(2007)
- Sheet---抽象(代表)Excel的表单页,可以设置选择某一sheet页或修改sheet页名字。
- Row---抽象(代表)Excel的行
- Cell---抽象(代表)Excel的单元格
- CellStyle---抽象(代表)单元格样式,比如背景颜色等等
- Font---抽象(代表)Excel的字体
我们通过POI操作Excel的时候其实就是先获取Excel的总长度,然后通过循环,处理每一行的数据,然后通过Row获取到一共有多少列,再次进行循环,获取到每一列的值,简单的总结就是无论是对Excel进行读取,还是对Excel进行写入,都会经历一次嵌套循环,我们需要自己组织从Excel读取的数据,或将我们原本的数据组织好,通过循环写入Excel中。因为Excel有两个维度,行与列,所以我们循环也就只有两层,分别操作行与列。
一般而言我们在项目中碰到Excel的操作,不是进行读取就是进行制作。
读取:一般都是用户上传Excel后对数据进行读取,可以通过
//根据上传流信息创建工作簿
Workbook workbook = WorkbookFactory.create(attachment.getInputStream());
的方式创建工作簿,即Excel的文档对象。
在日常开发中,经常发现,在server_name属性没有匹配上的时候,依旧能通过ip或localhost访问到nginx服务器,这与nginx的server_name匹配规则有关
在开始学nginx的时候server_name明明没有匹配上,但竟然访问到了,还以为server_name不起作用,后来发现server_name的匹配规则是:先遍历所有配置的server_name,如果找到了,则执行对应的server,如果没有找到,则默认执行第一个server。
即,当我们有个域名绑定的ip指向了nginx服务器的时候,无论如果即使没有匹配上任何一个配置文件中的server_name时,依旧会访问nginx配置的第一个server_name内容,而不会出现什么都访问不了的情况。
现在的方式:每次写之前先pull
,写完push
前也先pull
现在远程有一个仓库,分支就一个,是master。本地的仓库是从远程的master上clone下来的,再在自己本地改好,再commit → pull → push。
1,那我本地这个也算是个分支?还是就是一个本地仓库?
本地和远程的关系相当于两个分支,你感觉一样是因为你git pull 的时候已经自动给绑定好对应关系了
2,如果我在远程新建了个分支,然后我pull了下来,那我本地到底有分支这个说法吗?我本地的分支是不是就是那个远程新建的分支?
你远程新建了一个分支拉到本地的道理是一样的,属于复制了一份,但是本地分支和远程分支已经是两个东西了
3,本地仓库和本地分支有什么区别?
本地分支属于本地仓库里,是包含关系,一个仓库里可以有很多分支, 如果是 tag 的话可以分离出独立的仓库
4,commit是提交到本地仓库,然后push,这个push是把所有代码推到远程仓库,还是只是把commit的地方推到远程仓库?
肯定不会全量推送到远程的,是通过对比 commit 的记录,如果本地高于远程就直接把多出来的commit 给怼上去,如果本地的这几个 commit 和远程的 commit 有冲突的部分就merge,然后根据提交时间排序再新建一个merge 的 commit 记录再怼上去
5,那为什么要先commit,然后pull,然后再push,我pull了,岂不是把自己改的代码都给覆盖掉了嘛,因为远程没有我改的代码,我pull,岂不是覆盖了我本地的改动好的地方了?那我还怎么push?
这个先 commit 再 pull 再 push 的情况就是为了应对多人合并开发的情况: 1、commit 是为了告诉 git 我这次提交改了哪些东西,不然你只是改了但是 git 不知道你改了,也就无从判断比较; 2、pull是为了本地 commit 和远程commit 的对比记录,git 是按照文件的行数操作进行对比的,如果同时操作了某文件的同一行那么就会产生冲突,git 也会把这个冲突给标记出来,这个时候就需要先把和你冲突的那个人拉过来问问保留谁的代码,然后在 git add && git commit && git pull 这三连,再次 pull 一次是为了防止再你们协商的时候另一个人给又提交了一版东西,如果真发生了那流程重复一遍,通常没有冲突的时候就直接给你合并了,不会把你的代码给覆盖掉 3、出现代码覆盖或者丢失的情况:比如A B两人的代码pull 时候的版本都是1,A在本地提交了2,3并且推送到远程了,B 进行修改的时候没有commit 操作,他先自己写了东西,然后 git pull 这个时候 B 本地版本已经到3了,B 在本地版本3的时候改了 A 写过的代码,再进行了git commit && git push 那么在远程版本中就是4,而且 A 的代码被覆盖了,所以说所有人都要先 commit 再 pull,不然真的会覆盖代码的
6,两个分支,A和B,A合并B和B合并A,有区别吗?
两个互相合并的唯一区别就是 A->B 的时候 B 分支上会产生一个 merge_commit 的信息,这个时候 B 是合并状态而 A 未合并状态,如果现在没有发生任何改动执行 B->A 就直接切换过去了,连 merge_commit 都不会生成了
其实私钥生成时无需填写与用户名一致的邮箱,经过测试填写test,只要将公钥配置到gitlab上就能够使用ssh拉取更新项目了。
id_rsa这个私钥进行加密,然后推送到项目配置的地址,在云端会使用对应的公钥进行解密,如果能够解密成功那么就可以推送成功,与私钥公钥生成时的email没有任何关系!所以嘛我们在生成公钥私钥的时候也可以随意填写符合规范的字符串。比如我这次用的ssh-keygen -t rsa -C "test"
只要将这个公钥配置在github或gitlab上就能够使用ssh模式推拉代码。