实训概念总结业务概念:AOP日志统一处理基于JWT的登陆Nginx+Tomcat负载均衡KeepAlived总结【KeepAlived工作原理】支付宝支付消息中间件,RabbitMQ在项目中得使用:通过接口幂等性解决重复下单问题Zuul网关Eureka注册中心说说你在项目中咋使用Feign的Consul注册中心,配置中心的使用说说你对Hystrix的认识下订单接口安全接口限流缓存穿透缓存击穿缓存雪崩单例设计模式【手写,理解】手写线程手写分页技术概念:OSI七层模型:Mybatis中#和$的区别分库分表分布式事务当问你有没有用过 webservice或者dubbo时:当问你zookeeper时:当问你nacos时:关于Ribbon:HashMap的底层原理:线程安全的HashMap是ConcurrentHashMapSpringBoot运行原理:SpringMVC的运行原理:项目的生命周期/做项目的流程/你们平时是怎么开发项目的Hashtable与HashMap的区别 Set,List,Collection,Collections的区别?String,StringBuffer,StringBuilder的区别接口和抽象类的区别?重写和重载?多态的理解?Spring的理解:事务以及事务的传播特性和隔离级别【本地事务】SQL优化、索引tomcat调优JVM内存结构JVM调优线程池你是咋理解的/说说线程池线程相关概念说说你对锁的理解/java中你都用过哪些锁/数据库的锁/分布式锁说说自定义注解/用过自定义注解吗你项目中得哪些地方用过redis你在项目中用过redis中得哪些数据结构?redis的 持久化 策略都有哪些/为啥说redis是一个 可持久化 的 缓存 服务器?说说你对Docker理解Linux中你都用过哪些命令工作流A简历人事问题:【问一答一,不要主动说】职业素养【职场经验】:表达面试:实训概念总结
在项目中我做过后台管理系统的日志管理模块,日志管理模块的作用说白了就是记录用户的操作,这样就知道谁在什么时候干了什么事情。之前做其他项目的时候,日志管理模块通常都是在控制层结合log4j进行日志的控制台打印以及日志文件的存储,除此之外还会把日志信息插入到mysql数据库中存储起来,方便查看。
但这样做会有个问题,就是需要在每个Controller类的每个方法中都写上相关的日志记录代码,这样就会出现大量的代码重复,以后维护起来也特别麻烦。
所以说后面在做日志管理模块的时候,我就考虑到可以使用AOP做统一日志的处理,这样就可以让我们程序员在工作的时候把精力花在核心业务代码的处理上。
具体在做的时候我是这样写的。首先写一个日志切面类,这个切面类说白了就是一个普通的java类,后面会通过配置文件的配置让他具备切面类的功能。在这个普通的java类中我会自定义一个横切逻辑,说白了就是一个普通的方法,但这个方法中需要特别注意几点,因为当时在项目中我使用的是环绕通知,所以就会让这个方法返回一个Object类型值,再者就是在方法中有一个ProceedingJoinPoint类型的参数。
具体在写方法的时候有几个特别关键的地方,首先因为要获取当前登录的用户,用户的信息是存储在session中的,我当时采用的解决方案是使用ThreadLocal+Filter来完成在一个普通的java类中获取当前请求的request对象,进而获取存储在session中的用户信息。ThreadLocal可以把它理解成一个Map,但它特殊的地方就是它用当前线程充当key,所以在使用的时候,存储信息用set(value)就行了,之所以没有写key,就是因为当前访问的线程就是默认的key,同理取数据用get();当时我封装了一个工具类,工具类中有个setRequest方法,就是将request对象存储到ThreadLocal中,同样还有个getRequest方法,就是获取当前线程对应的reqeust对象。
之后我会在自定义的Filter中的doFilter方法中,调用工具类的setRequest方法,将当前请求存储到ThreadLocal中,当然还得在web.xml中配置Filter使其生效。接下来我会在日志切面类中通过调用工具类的getRequest方法来获取request,进而通过getSession来获取session,这样就可以取到存在session中的相关用户信息了。
获取了用户信息之后就要记录用户做了什么事情,在这块我们当时的项目是这么规定格式的,就是要记录用户执行了哪个类的哪个方法,并且要把执行这个方法时候对应的参数信息也给获取到,比如用户添加了一个商品,那记录的信息就应该是调用了ProductController类的addProduct方法,并且也要获取到添加的商品信息参数,这样才能看的更明白。
获取类名和方法名这块是通过反射机制,调用ProceedingJoinPoint的相关方法获取的,获取参数信息是通过request.getParameterMap()之后对其进行循环遍历,这样就获取到了提交时候的参数详情.
在这整个切面类中还有一个特别重要的方法就是ProceedingJoinPoint.proceed();它代表的就是实际要执行的 核心业务逻辑,它的返回值就是实际执行方法的返回值,比如刚才说的ProductController类中的addProduct方法, 这个proceed()代表的就是addProduct方法,而他的返回值就是控制层中addProduct方法的返回值。之所以 它的返回值类型为Object就是因为不同方法的返回值不一样,但它们都属于Object对象。
最后需要在spring的配置文件中配置aop:config以及配置切点表达式来对控制层中的增,删,改方法进行拦截,这里就用到了切点表达式中特殊符号双竖杠||。
在上交完任务后,我们经理给我说这个东西,做的整体上还不错,但是有个问题,就是日志虽然记录了操作哪个类的哪个方法,程序员可以读懂,但业务员根本就看不明白,不够人性化,让我的日志记录再改进下。
我通过和我们团队的人讨论,最终决定通过自定义注解来完成这个改进。
在写自定义日志注解时候,通过 @Target 设置为Method指明该注解只能用在方法上面,通过将 @Retention设置为RunTime指明将注解保留至运行时。这样就可以通过反射去获取注解信息。
在注解中声明了一个String类型的value来让程序员手工设置日志的信息,之所以采用value,是因为value这个字段有特殊的含义,它可以在使用自定义注解给日志信息赋值的时候省略不写,用起来更加方便。之后就可以在Controller中的方法上加入自定义注解并且对value进行日志信息的赋值,如 @Log("增加商品")。
最后在AOP的日志切面类中通过获取方法签名得到Method,通过Method的isAnnotationPresent判断该方法上面是否加入了自定义日志注解,如果 是 则再通过Method的getAnnotation来获取自定义的日志注解,最后再通过.value()方法获取自定义注解中日志信息的值,这样在记录日志的时候就可以显示更加人性化的信息。
JWT也就是Json Web Token,它的核心工作过程是这样的,在用户成功登陆后,会将用户的相关信息响应到客户端,在客户端保存用户信息,然后当用户再次发送请求时,会将用户信息作为请求头传递到服务器端。因为用户信息都是在客户端,所以就不会导致大量用户登陆造成的服务器端内存占用越来越多的问题。再者不管前端是手机App还是WebApp都可以根据接受到的用户信息按照自己的方式进行处理,只要再次发送请求的时候把他作为请求头传递到服务端就行了。但是将用户信息响应给客户端也会有问题,那就是信息可能会被篡改,毕竟客户端是不安全的。为了防止信息被篡改,我们可以通过签名的方式来解决。但签名的时候也要注意,需要在服务端结合秘钥对信息进行签名,因为秘钥是在服务端存储,所以攻击者就不可能获得秘钥,这样就保证了签名的安全性。再者就是在用户后续发送请求的时候,会将用户信息和签名作为请求头传到服务端,而服务端需要对传过来的请求头进行验签,从而确定信息没有被篡改。所以JWT的核心说白了就是签名和验签的过程。
我来说下我之前做的项目中负责过的功能。因为我们现在基本上都是进行前后端分离开发,我一般都负责后端接口的开发,之前有做过基于JWT的登陆。之前我们进行项目开发的时候,因为没有进行前后端分离,这时候做登陆比较简单,就是验证完相关的信息后,将用户信息存入到session中,然后在拦截器这块判断session中是否有用户信息,如果有,则证明是用户成功登陆了,那咱就放行,如果没有,则证明是非法用户,就跳转到登陆页面,让他进行登陆。但这种基于session的登陆方式,在进行前后端分离开发的时候就不合适了。因为session是存储在服务端内存中的,所以随着登陆用户量的增加服务端的内存也会被越来越多的占用,那么就会导致服务端的性能下降;再者我们写接口也是为了能够适应各种各样的前端,不管是手机App还是WebApp。如果是手机App,使用session就很不方便。所以在前后端分离开发时,关于登陆这块我们会使用JWT的这种方式。使用JWT还可以避免在项目进行负载均衡部署时产生的session漂移问题,因为JWT是将登陆用户的信息响应到客户端,所以就不存在session漂移的问题。
实现JWT的第三方技术框架有很多,比较常用的就是JJWT。但在我们的项目中并没有使用它而是我们根据JWT的内部原理,自己封装了一套,这样我们就能够在项目中根据我们的业务需求更灵活的进行使用。
具体我们是这样做的。首先在用户登陆的时候,我们进行用户名,密码的非空验证,包括验证用户名和密码的正确性,之后我们会根据用户名查找到对应的用户信息,然后根据我们的业务需要将需要响应到前端的用户信息封装成一个对象,并将它转换为json格式的字符串。这时候要特别注意不能将密码这种敏感信息封装到对象中。之后我们通过md5散列算法结合秘钥对json格式的字符串进行签名。最后我们会将json格式的字符串和签名分别都经过base64的编码,然后中间用.分割,响应给前端。在这个过程中还有个需要特别注意的,因为考虑到jwt不能是永远一直有效的,而是也要有存活时间,所以还需要根据会员id作为key,value值可以是空字符串,设置过期时间,比如30分钟,然后将其存入redis服务器中,通过redis提供的setEx来完成。
当 前端 再次发送请求时候,会将用户信息和签名作为请求头提交给服务端。这个请求头可以自定义,比如我们可以叫x-auth或者x-token都行。我们在服务端通过拦截器来验证每次请求时候发送的请求头是否正确。具体过程是这么做的。因为有的请求即使不登陆也能进行访问,比如登陆,注册,所以为了区分哪些请求需要登陆后才能访问,哪些请求可以不登录就能够直接进行访问,我当时通过一个自定义注解比如叫@check,把这个自定义注解加到需要登陆后才能访问的接口上,这样在拦截器中,可以获取方法上是否有该注解,没有该注解的直接放行,有这个注解的才进行后续的验证操作。再者为了解决前后端分离的跨域问题,我们最开始的时候是在每个controller类上加入了@CrossOrigin注解,但现在有了拦截器,所以我们的处理方案是把以前各个controller上的@CrossOrigin注解去掉,然后在拦截器中加入response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,"*"),这样就可以在拦截器中统一处理跨域问题了;再者因为发送了自定义的头信息,所以在每次发送请求前,都会先发送一个options请求,而这个options请求并不是我们想要处理的真正请求,所以我们在拦截器中通过request.getMethod()获取请求方式,判断如果是options请求,则直接return false,拦截返回即可。还有刚才也说到客户端提交的头信息中,有我们自定义的x-auth,所以针对自定义的头信息我们也需要在拦截器中通过response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "x-auth")来使其可以接受自定义的头信息。处理完这些之后,我们就可以根据请求是否需要登陆来进行验证了,具体验证的时候,我们通过request.getHeader()获取自定义的x-auth,看是否有值,如果为空,则提示前端,头信息丢失;如果有值,再根据.进行分割,看分割后产生的字符串数组的长度是否为2,如果不为2,则提示头信息不完整;当头信息有值并且内容是完整的之后,我们就可以获取用户信息的base64内容和签名的base64内容,然后将他们两个都进行base64解码,将解码后的用户信息通过md5散列算法结合秘钥再次进行计算得到新的签名,看这个新签名和客户端提交过来的签名是否一致,如果不一致则提示验签失败,如果正常则将存在redis中会员进行续命,也就是重新设置以会员id作为key的内容,过期时间重新设置为30分钟,通过redis提供的expire来完成即可。最后为了能够在接口中获取登陆的用户的信息,所以还会将通过验证的用户信息存入request中,这样在接口中就可以通过request.getAttribute来获取登陆的用户信息了,紧接着放行即可。在这里面还有个需要注意的,因为拦截器中的返回值是boolean类型,所以我们在响应给前端的信息需要是json格式的字符串,这两个类型是不一样的,所以我们的解决方案是,自定义一个异常比如叫TokenException,然后再写一个异常统一处理器,在异常统一处理中捕获到TokenException,然后获取异常中的错误信息和编码结合@responseBody将其转换为json格式字符串响应给客户端,而在拦截器中遇到验证不通过的话就可以通过throw抛出自定义异常并设置对应错误信息和编码。这就JWT登陆验证的整个过程。
在项目开发完成后,我们需要将其部署上线。我们在部署的时候通常是用tomcat来作为web应用服务器处理动态资源的请求,用nginx来作为web服务器,将静态资源比如js,css,html等放到nginx上面,还会使用nginx作为反向代理服务器结合多台tomcat搭建负载均衡。
之所以要这么处理,是考虑到服务器的高可用以及处理高并发的问题。如果最终项目只使用1台tomcat的话,那么会产生两个问题,第一,如果这台tomcat宕机了,那么项目就访问不到了;第二,一台tomcat能够承受的并发量是有限的,基本上在1500左右,如果在遇到高并发比如同时过来了3000个请求,那么这一台tomcat处理起来就比较费劲,会让用户感觉系统反应特别慢,甚至出现卡顿的情况。
使用负载均衡后,我们可以让一台nginx后跟多台tomcat,这样即便一台tomcat宕机了,还有其他的tomcat能够继续提供服务,这就是高可用;当并发量大的时候,我们的请求通过nginx被分发到了多台tomcat进行处理,这就增加了处理高并发的能力。
具体在做的时候比较简单,我大概记得是这个样子,首先将项目分别部署到多台tomcat中,然后在nginx.conf配置文件中,找到server这块的配置信息,找到里面的location /,在里面添加proxy_pass 后面跟上http://upstream对应的名字,比如upstream后的名字叫shop-api,那就写proxy_pass http://shop-api,最后通过配置upstream来指向需要关联的多台tomcat。如果我们部署的是springboot项目,因为springboot项目最终打成的是jar包,所以在linux上启动jar包项目的话,为了能够在启动后生成日志文件并且能够以后台模式运行,我们会通过nohup java -jar jar包的名字 & 来实现。
我们会把我们的后台管理项目,也通过nginx+tomcat负载均衡的方式进行部署,这样是为了增加项目的高可用,避免因为一台tomcat宕机而导致的整个服务不可用。但在部署后产生一个问题:就是你会发现明明你输入的用户名和密码都正确但是就是登陆不成功。说白了这就是因为session漂移导致的。之所以会产生session漂移的问题,是因为默认情况下nginx是轮询的负载均衡策略,又因为对于后台管理项目来说,我们在用户登陆成功后会将用户信息存入到本地session中。比如一台nginx后跟了两台tomcat,分别是t1和t2;当用户输入正确的用户名和密码,点击登陆发送请求的时候,请求经过nginx被分发到了t1,经过验证后,将用户信息就存到了t1的session中,但登陆成功后也会跳转到其他页面发送新的请求,而因为默认是轮询策略,所以新的请求经过nginx可能会被分发到t2上,t2上部署的项目,在拦截器这块发现从本地的session中获取不到对应的用户信息,因为在登陆成功后将用户信息存到了t1的session中,所以就又跳转到了登陆页面。
想要解决这个问题可以把nginx的负载均衡策略设置为ip_hash,就行了。ip_hash的原理是这样的,当ip地址为192.168.1.2的客户端发送请求到nginx的话,nginx会将ip地址进行hash化,如果第一次nginx把这个请求分发到了t1上,那么就会将这个hash化后的值和t1进行绑定。以后192.168.1.2客户端发送的任何请求都会被nginx分发到t1上,这样就解决了上面的session漂移问题。
但ip_hash在解决session漂移的时候还不是很完美,如果t1这台tomcat宕机了,那么当客户端再发送请求的时候,nginx就只能将新的请求分发到t2上,而t2上这时候并没有对应的session信息,所以经过拦截器还是会被跳转到登陆页面,这时候就可以通过JWT来完美的解决session漂移的问题,因为JWT是在用户登陆成功后,将相关信息响应给客户端保存的,所以就不会存在刚刚说到的那些问题。
当这个负载均衡搭建完毕后,还存在一个问题,就是nginx单点故障,因为在目前的服务器架构中,nginx只有一台,如果这台nginx宕机了,那么即使tomcat都正常运行,也不能对外提供服务了,所以为了解决这个问题我们采用了KeepAlived来实现nginx的高可用。
在多台nginx/负载均衡器安装KeepAlived,然后选出一台nginx作为主服务器,其他nginx就是备份服务器;主服务器和备份服务器都有共同的VIP;所有客户端的请求,都发送给VIP,然后有VIP转发到主服务器上,然后再由主服务器转发到多台tomcat。当主服务器宕机后,VIP就漂移到备份服务器上,那么所有客户端请求通过VIP转发到备份服务器上,由备份服务器再转发到多台tomcat上。当主服务器又能正常工作了,VIP又会漂移到主服务器上,那么请求就会通过VIP转发到主服务器上,再由主服务器转发到对应的tomcat上,这就是整个工作的过程。
比如现在我有两台Nginx,ip地址分别是1.10和1.11;VIP设置为1.100。在这两台nginx/负载均衡器安装KeepAlived,设置1.10为Master,设置1.11为Backup。Master服务器 定期 会给备份服务器发送消息,比如【我还活着】,这个就是所谓的【心跳】,VIP会漂移到主服务器上(说白了VIP和咱们的主服务器绑定了,发送到1.100的请求都被实际发送到了1.10这台服务器上,再由1.10上的nginx将请求转发给对应的tomcat),当备份服务器收不到主服务器发送的心跳时,它就认为主服务器宕机了,那么VIP漂移到备份服务器上,那么所有客户端请求通过VIP转发到1.11这个备份服务器上,由备份服务器再转发到多台tomcat上。同样当Master正常后,又能正常给备份服务器发送心跳了,所以VIP漂移到Master上。
我之前在项目中负责过支付宝支付这块的任务。在做这个任务的时候,代码这块其实并不是太难,关键是对整个流程的梳理以及和项目现有业务的结合。
这里面主要涉及到的是通过商家注册,创建应用获取对应的appid,生成商家的公钥私钥以及通过提交商家的公钥在支付宝中生成对应的公钥和私钥。
然后在商户平台的应用中配置appid,支付网关,商家私钥,支付宝公钥以及notifyUrl和returnUrl地址。
我开发过程中是通过支付宝提供的沙箱环境来进行的,所以会将刚才提到的在商户平台应用中配置的那些信息按照沙箱环境的要求进行修改。真正部署上线后改成正式的配置就行了。
notifyUrl:就是在支付成功后,支付宝这个支付平台回调的url,必须是post请求,最终在这个url对应的controller类的方法中“处理商家自己的业务”,因为notifyurl会被支付平台进行回调,所以即便买家关闭了浏览器,只要商家的项目还在正常运行,那么就会被支付平台成功调用,就会执行里面的业务。在notityUrl对应的控制层的方法中如果处理成功要返回"success",如果商家平台响应的是“fail”则支付平台会重新发送请求,最多情况下会发送8次请求。重发可能会导致这么一个问题,商家平台的业务执行成功了,但是在给支付平台进行"success"响应的时候,因为网络问题,导致支付平台没有收到"success"响应,那么支付平台就会再次调用notifyUrl,这时候就会出现接口幂等性问题,可能会导致积分重复添加,销量重复累计;我们可以根据订单的状态来进行幂等性处理。
如果当订单的状态是“未支付”的时候,才正常执行相关的业务,否则就证明已经执行过了,就不需要再重复执行了。因为notifyUrl是需要在外网能被访问的,只有这样支付宝平台才能对他进行调用,在项目真正部署上线的时候,notifyUrl就是一个能被外网访问的地址,但在开发阶段,为了达到能被外网访问的目的,我是通过一个叫做NatApp的软件来实现的。
returnUrl:支付成功后,跳转到到url,必须是get请求,最终跳转到“支付成功”的页面,不建议在returnUrl中写相应的"商家业务逻辑的处理",因为买家可以在支付后,直接关闭浏览器,那么returnUrl中的“商家业务逻辑”就不会被执行,所以在returnUrl中写“商家业务逻辑的处理”是不靠谱的。
在这整个过程中比较重要的就是要保证传递数据的安全性,也就是保证数据不能被篡改,关于这块实际上就是通过RSA这种非对称加密算法中的公钥和私钥来实现的。
商家平台:有商家自己的私钥以及支付宝的公钥
支付平台:有商家的公钥以及对应的支付宝私钥
商家平台向支付宝平台发送请求,进行支付的时候,向支付宝平台提交了和订单相关的明文信息比如订单号,订单金额,以及对应的签名信息,签名的作用就是为了保证数据不被篡改。生成签名的时候是根据商家的私钥对订单的明文信息进行加签,当支付宝平台接受到请求后,就根据商家的公钥对签名进行验签,如果验签成功则证明数据没被篡改,那么跳转到支付宝平台的支付页面,扫描二维码就可以进行支付了,如果验签失败那么就提示相关的错误信息;当用户扫码支付成功后,支付宝平台就会调用商家平台的notifyUrl接口并传递相关的参数信息,为了保证这些信息在传递过程中得安全性,那么就会在传递前通过支付宝平台的私钥对其进行加签,商户平台为了保证传递的信息在过程中没有被篡改,所以就会用支付宝的公钥对其进行验签,验签成功后就会执行商户平台的相关业务比如更改订单状态以及给会员加积分,之后返回“success”,如果验签失败则返回"fail"。
在我们做项目的过程中,要考虑到处理高并发时性能这方面的问题,解决性能问题的方法有很多,通过消息中间件就可以起到异步,削峰填谷的作用,从而提高性能。
就拿我之前项目中负责过的发送邮件来说吧,这个功能非常常见也非常简单,说白了就是我自己写个邮件工具类,在需要用到的地方去调用工具类中发送邮件的方法就行了,传递收件人,内容,标题等等。在项目中,比如用户注册成功,发送邮件进行激活;还有用户找回密码的时候,通过发送邮件将新密码发给用户注册时候的邮箱,等等都需要用到。
这个时候如果用传统的方式做也就是同步发送邮件,那么会导致在高并发情况下,性能低下;也会因为紧耦合而出现邮件发送失败的话,用户注册也会失败的问题。这个时候我们就可以借助于消息中间件来解决。在项目中我们使用的是RabbitMQ。
使用消息中间件,是这样做的,首先我会通过docker的方式来安装rabbitMQ,然后启用后台管理的插件,这样就可以通过浏览器访问rabbitMQ的管理界面,从而看到交换机,队列,消息等相关信息。rabbitMQ最经常使用的端口号是15672以及5672,其中15672是访问后台管理界面时候使用的,5672是项目连接rabbitMQ时使用的。
通过rabbitMQ我们将之前紧耦合的同步发送邮件就可以变成异步发送邮件。这样在用户注册后,不是直接调用工具类中得方法同步发送邮件,而是将发送邮件时候用到的相关信息比如标题,收件人,内容,转为json格式的字符串,作为消息体发送到消息中间件中。通过这种异步的方式,首先可以提高注册会员的性能,再者就是即便发送邮件失败,也不会导致会员注册失败,因为我们在注册会员时,只是将相关消息发送到消息中间件中,并没有直接发送邮件;然后会新建一个项目作为消费者,在消费者中通过@RabbitListener读取消息队列中得消息,再去进行邮件发送的工作。这样在项目中所有需要发送邮件的地方都可以直接调用发送消息到消息中间件中得方法,使用起来也会更加便捷,方便。
在具体的代码方面是这样的,通过生产者发送消息到交换机,交换机通过路由key将消息发送到绑定的队列上,消费者再通过监听消息队列从而获取消息进行业务处理。根据项目的业务场景不同,使用不同的交换机,如果是这种一对一的方式的话,那么会使用DirectExchange,这种类型的交换机在和队列进行绑定的时候需要指定路由key,我们在发送消息的时候通过指定交换机的名字以及路由key就能将消息发送到绑定的队列中;如果是要实现一对多方式的话,可以考虑使用Fanout类型的交换机,对于这种交换机在和队列绑定的时候是不需要指定路由key的,因为Fanout类型的交换机会将消息转发到所有绑定的队列上去,从而实现类似于广播的特性。
异步可以提高性能,就拿注册会员发送邮件来说,如果采用原来传统的同步的方式,那么注册会员插入数据库需要50ms,发送邮件需要50ms,完成整个会员注册的工作就需要100ms,对于会员注册来说每秒钟的请求数是1000/100=10个请求左右;但是如果采用异步的方式,注册会员插入数据库还是50ms,异步发送邮件说白了就是只需要将消息发送到消息中间件的队列中就行了,比如需要10ms,这时候完成整个会员注册的工作就需要60ms,对于会员注册来说每秒钟的请求数是1000/60=16个请求左右,所以异步确实可以提高性能。
同样在项目中还涉及到支付订单成功后,给会员增加积分以及给对应的商品增加销量,也可以把这个改成异步操作,说白了就是在支付成功后,将消息发送到消息中间件中即可,然后消费者再读取消息,进行业务处理。这样就可以提高支付的性能。为了更加的提高性能,在这里我采用的是Fanout类型的交换机,并且在这个交换机上绑定两个队列一个是积分队列,一个是销量队列,这样消息就会通过Fanout类型的交换机被发送到这个两个队列中,分别监听这两个队列,就可以同时加积分和加销量,所以可以更高的提升性能。
在项目中,我们还要解决下单后,超时未支付,自动取消订单的功能。对于这个功能可以通过定时任务每隔指定的时间扫描订单表,根据订单的下单时间和当前系统时间做对比,找到需要取消的订单,更改订单的状态并进行相关的业务操作,但通过定时任务有个问题,就是你指定的时间间隔到底多少合适,如果指定的过大,会导致有些订单明明已经到了该取消的时候,但因为还没到扫描的时间导致取消订单的操作严重延迟;如果指定的过小,会导致频繁扫描订单表,从而因为过于频繁访问数据库而导致性能降低。
为了能够及时关闭超时未支付的订单以及减少数据库的压力,可以结合消息中间件中得死信队列来实现。说白了死信队列就是一个普通的队列,死信交换机就是一个普通的交换机。整体的业务是这样处理的。我们会声明一个direct类型的交换机叫order_exchange,再声明一个业务队列比如叫order_queue,将交换机和业务队列进行绑定,指定路由key,叫order_route_key,之后我们在业务队列order_queue中设置死信交换机为dead_order_exchange,设置死信路由key为dead_order_key;再创建一个死信队列dead_order_queue,说白了就是一个普通的队列,将这个死信队列通过路由key,dead_order_key和死信交换机绑定;然后在下订单成功后发送消息到普通队列,并设置消息的过期时间为1个小时,这样当1个小时到期后,消息就会从普通业务队列order_queue中跑道对应的死信队列dead_order_queue中,我们新建一个消费者,通过@RabbitListener监听死信队列,一旦有消息就取出消息,根据消息中得订单id,将指定的订单状态设置为“交易关闭”,并归还对应的商品的库存。当在1个小时内支付订单后,我们将订单的状态更新为“已支付”,这时候队列中还有这条消息,当1个小时到期后这条消息还会跑到死信队列中,所以为了避免消息的重复消费,那就需要进行消息的幂等性处理,我的解决方案就是消费者这块在获取死信队列中得消息后,进行业务处理时,只是将订单状态等于“未支付”的订单更新为“交易关闭”状态,如果订单状态不是“未支付”而是其他的“已经支付”,“已发货”,“交易关闭”等状态则不去进行处理直接跳过。
如何保证消息100%投递成功,消费成功?/如何保证消息不丢失?
想要保证消息不丢失,得从这几个方面入手。咱们都知道,消息发送的过程是从生产者到Exchange交换机,再从交换机到Queue队列,最后再由消费者读取队列中得消息,进行业务处理。所以要想保证消息不丢失,就得分别保证生产者能够将消息发送到交换机;交换机必须保证把消息发送到队列;消费者必须能够成功消费消息;这三个阶段都得成功才行。在项目中我们可以结合confirmCallback回调函数来保证生产者将消息成功发送到了交换机;结合ReturnCallback回调函数来保证交换机将消息成功发送到了消息队列中;最后通过手动Ack的方式来保证只有消费者在成功处理完业务后才会删除消息队列中对应的消息。
如何解决消息被重复消费的问题?/消息幂等性问题?
削峰填谷的优点是啥?/削峰填谷你是怎么理解的?
削峰填谷的目的也是为了应对高并发。咱们可以这样想,如果高并发的请求直接访问数据库,比如秒杀,那么数据库的压力瞬间会达到最大,甚至导致数据库宕机,而通过加入消息中间件,当大量的请求过来后,先将对应的消息直接发送给消息中间件而不是直接打到数据库上,这样就缓冲了高并发所造成的压力,相当于削峰,然后我们可以再写一个消费者从消息队列中不断的读取消息进行处理,这就相当于填谷。说白了通过削峰填谷可以让系统更加平稳的处理高并发,不至于累的时候累死,闲的时候闲死。
异步的优点是啥?/异步你是怎么理解的?
异步可以提高性能,就拿注册会员发送邮件来说,如果采用原来传统的同步的方式,那么注册会员插入数据库需要50ms,发送邮件需要50ms,完成整个会员注册的工作就需要100ms,对于会员注册来说每秒钟的请求数是1000/100=10个请求左右;但是如果采用异步的方式,注册会员插入数据库还是50ms,异步发送邮件说白了就是只需要将消息发送到消息中间件的队列中就行了,比如需要10ms,这时候完成整个会员注册的工作就需要60ms,对于会员注册来说每秒钟的请求数是1000/60=16个请求左右,所以异步确实可以提高性能。说的直白点异步就相当于现实生活中得送快递,如果是同步的工作方式,则快递员需要将快递要亲自交到手收货人的手里后才能继续送下一个快递;显然这样的性能是很低的,如果是异步就变成了快递员只需要将快递送到菜鸟驿站就行了,这样就可以马上去送下个快递,工作效率会提高很多。
解耦的优点是啥?/解耦你是怎么理解的?
A服务调用B服务,如果是紧耦合,当B服务宕机或者抛异常,就会直接导致A服务也不能正常运行;这时候可以通过消息中间件来解耦,这样就可以用A作为生产者 将 消息 发送到消息中间件中,B作为消费者从消息中间件中获取消息,然后进行相关业务的处理,这时候即便B宕机或者抛异常,也不会影响到A。当B恢复正常后,还能够读取消息队列中得消息,从而执行相关的业务。解耦后,就从同步执行变成了异步执行,提高了性能;解耦后,如果又需要调用新的C服务了,就不需要在A系统中再去写任何调用C服务的代码了,只需要C服务去监听对应的消息队列,获取其中的消息,进行相关业务的处理就行了。
我在项目中做下单这块的时候,遇到了这么一个问题,正常操作情况下都没啥毛病,但如果通过jmeter进行压测时,就会出现重复下单的问题。
后来我自己考虑到应该是接口幂等性的问题;幂等性这块比较简单,说白了,就是调用接口一次和多次如果产生的结果是一样的,那就说明这个接口是幂等性的,否则就说这个接口不具有幂等性。就拿咱们平时的增删改查来说,查询和删除操作就天生具备幂等性,但增加和更新操作就不具备幂等性。
之所以会出现高并发情况下重复下单的问题,说白了,就是因为接口没有进行幂等性处理,结果导致多插入数据了。我最终是通过redis+token的方式来解决的。
具体的思路是这样的。
首先我会写一个创建token的接口,这个接口的代码也特别简单,就是生成一个uuid作为token,然后响应给客户端,并且将该token存储到redis中,而且设置它的过期时间为10分钟,这样在客户端中就可以调用这个接口生成一个唯一的token标识,并在后续的请求中将该token作为header中的信息传入到接口的服务端进行相关验证。
接着我写了一个自定义的注解,这个注解没有什么实质性的内容,说白了就是一个标识,只要是加上了这个注解的方法,都会在我自定义的拦截器中进行幂等性的处理,否则就说明这个方法不需要做幂等性处理,那么在拦截器中就会放行。
幂等性拦截器中的判断是最核心的。首先我会判断请求中是否包含token头信息,如果没有就响应给客户端一个提示,头信息丢失。之后就是调用redis的delete方法,将该token作为key进行删除操作,如果返回的值等于0,则证明有其他请求之前已经发送过了,该请求就不是第一个发送过来的请求,就提示请求重复,返回1,就证明是第一个请求,放行即可。
之后我会在下订单的方法上加入自定义的幂等性注解,就解决了重复下单的问题。
之前我们在做项目的时候,是把所有的接口都写到一个API接口项目中,那时候处理接口安全问题,我们是写了一个AOP切面或者拦截器,统一对控制层的各个方法进行安全验证,但现在用到SpringBoot+SpringCloud微服务,每个功能模块都是一个单独的微服务项目,如果还用之前AOP切面或者拦截器的方式,就会出现很多重复代码,维护起来也特别麻烦。
因为外部客户端的请求都是通过Zuul网关路由到具体的微服务,所以为了保证微服务的安全,我们就在Zuul中自定义了过滤器,对所有微服务的安全进行统一的处理,还有,因为涉及到前后端分离,前端项目访问后端微服务涉及到跨域问题,所以我们在Zuul中也自定义了关于跨域的过滤器,进行统一处理。
具体是这么做的,首先你得先定义一个继承于ZuulFilter的类,重写里面的filterType()方法filterOrder()方法,shouldFilter()方法,还有一个最核心的用来写具体业务逻辑的run()方法。其中filterType方法的作用是用来返回一个字符串,指明该过滤器的类型,经常用到的有pre类型,说白了就是在请求被发送到微服务之前调用;我们的微服务安全认证以及跨域这块都是用的pre类型的过滤器,这样对非法请求,就可以在发送到具体的微服务之前拒绝它;
还有post类型,说白了就是微服务执行完后再执行该过滤器,filterOrder()方法返回一个int类型的值,用来指明该过滤器的执行顺序,数字越小表示优先级越高,就越先执行,shouldFilter()方法返回一个boolean值,用来指明该过滤器是否执行,true表示执行,false表示不执行。
run方法中就是之前说的,用来放具体的处理逻辑。
在run方法中,首先获取一个RequestContext对象,之后就可以通过调用它的getRequest()方法来获取request对象;这样就可以获取头信息,按照基于token的方式进行接口的安全验证。
过程中特别需要注意的就是当验证不通过的时候,需要通过fastjson将要响应的数据转换为json格式的字符串,之后设置响应的内容类型为application/json并且指定utf-8的编码方式,用来处理中文乱码问题;通过setResponseBody将Json格式的字符串设置为响应的内容,最后通过setSendZuulResponse为false,禁止路由转发。如果想要将zuul过滤器中的数据传递给后端微服务中使用,则需要通过addZuulRequestHeader方法来进行,而后端微服务中就可以通过request.getHeader来获取值。这里面需要特别注意的就是,如果传递的数据中含有中文则需要通过URLEncoder进行utf-8的编码,同样在获取数据后也需要通过URLDecoder进行解码。
最后想要使自定义的过滤器生效,得进行相关的配置。我通过创建一个配置类,
并且在类上通过 @Configuration 和 方法上的 @Bean 结合起来,完成自定义filter的配置。
我们在做微服务开发的时候使用Eureka来充当注册中心,首先需要在pom文件中引入Eureka的依赖,之后在启动类中加入 @EnableEurekaServer的注解证明该项目的作用是作为注册中心使用,并且在对应的application.yml中指明Eureka注册中心服务器的地址,方便提供者和消费者对其进行访问,为了增强注册中心的高可用性,我们做了Eureka集群,防止单台Eureka导致的单点故障问题。
在我们的项目中,为了达到微服务高可用的目的,我们会将每个微服务都部署多份,就相当于为每个微服务做了个集群,并且微服务在启动后都会根据配置文件中配置的spring.application.name的值来作为微服务在注册中心里的名字,根据配置文件中注册中心的地址,来将微服务注册到注册中心中,这样我们的前端项目访问zuul网关,zuul网关中配置微服务的名字和路径之间的对应关系,这样当请求发送过来后,zuul就根据请求的路径找到对应的微服务,再根据微服务的名字从注册中心中找到该服务对应的地址列表,默认会采用轮询的负载均衡策略,去访问地址列表中具体的微服务; 而且在通过Feign进行微服务之间的调用时,也是指明微服务的名字,然后通过微服务名从注册中心中获取对应的地址列表,进行具体的访问。
如果没有注册中心,客户端直接通过ip地址访问服务端,如果服务越来越多,调用也会越来越复杂,一旦ip地址发生变动,则所有用到的地方都需要发生改变,维护起来会特别麻烦;如果服务端宕机了,所有访问的客户端也都会受到影响。通过注册中心可以让服务端注册到注册中心上去,并且可以进行负载均衡;这样就可以达到高可用以及提高并发的 目的。客户端 也不需要 直接 调用服务端,而是通过注册中心获取地址列表,从地址列表中选一个服务端接口进行调用,后续的维护各方面也会比较方便。
我们在项目中是用Feigin进行微服务之间的调用的,又因为Feign集成了Hystrix,所以我们当时通过在配置文件中开启hystrix,并且在具体使用Feign的接口上通过 @FeignClient 中的 Fallback指定降级时候需要执行的类即可。这样在熔断后就会自动调用Fallback类中对应的降级方法,降级方法中,可以根据业务需求返回默认值。
Feign还拥有负载均衡的特性,说白了它是靠Ribbon进行的负载均衡。在 @FeignClient的注解上指明要调用的微服务的名字,这样就可以通过该服务名从注册中心中获取对应的地址列表,方便进行负载均衡的调用。
其次我们会创建对应的服务端,服务端项目中要保证 请求的方式,请求的地址以及方法名,返回值,参数都要和feign中定义的保持一致。
最后一定要在客户端的入口类上加入@EnableFeignClients的注解,从而使Feign的配置起作用。
在项目中除了可以通过Eureka来做注册中心,还可以使用Consul来充当注册中心,Eureka是通过在一个SpringBoot项目中导入相关的jar包依赖,通过启动这个项目来起到注册中心服务端的作用;而Consul是一款软件,通过启动这款软件来起到注册中心的作用。Consul在注册中心方面和Eureka起到的作用都差不多,都是为了让多个微服务作为注册中心的客户端注册到注册中心上去,从而可以通过微服务的名字获取对应的地址列表进行相关的访问,方便管理和维护,也可以通过访问微服务的名字进行负载均衡。
Consul还可以充当我们微服务项目中的配置中心。
如果没有配置中心,那么每个微服务中都需要写一个相关的配置文件,而且这些配置文件中还会有一些重复的部分,如果后续修改,则需要更改多次,并且每次修改完之后还得重启微服务,才能让配置文件生效,而我们可以通过Consul来充当我们的配置中心,将所有相关的配置都放到Consul中,以key,value的方式;这样就能够起到统一管理的作用;所有的微服务都读取配置中心中相关的配置信息。当配置文件的内容需要修改时,直接修改Consul配置中心里的内容即可,微服务在不需要重启的情况下,就能读取更改后的最新信息。
具体在使用的时候通过在consul的key/value配置中,建立指定的Key,这里的key要和微服务配置文件中的配置信息对应;而且微服务的配置信息要放到bootstrap.yml的配置文件中才行;value其实就是对应的配置信息,可以是properties的格式也可以是yaml的格式;在读取配置中心的配置信息时为了能够达到配置中心的内容改变后,各个微服务在不重启的情况下获取最新的配置信息,则需要结合@ConfigurationProperties注解,将配置文件中得信息读取到一个javabean中,并且在启动类上加上@EnableConfigurationProperties从而让指定的配置类起作用;这样在需要用到配置信息的地方就可以通过@Autowired将需要的配置类注入,然后就可以通过访问属性来获取对应的配置信息。
在整个过程中要指明配置中心的ip地址和端口号,默认连接的端口号是8500.
在分布式微服务的项目中,经常会存在微服务之间的相互调用问题;这个时候可能会出现微服务的雪崩问题;说白了就是A调用B,B调用C,如果C这个微服务出现问题可能会级联影响到B,同样B出现问题也会级联影响到A,这样就会产生一个微服务出现问题从而导致其他微服务也不能被正常访问。
为了解决这个问题,可以在项目中融入hystrix来解决;hystrix有三大特性,熔断,降级和资源隔离。
熔断的话我们可以把它理解成家里面的保险丝,咱们都知道保险丝的作用就是在电器出现问题时,断开,从而避免家里的其它电器受到牵连。hystrix的熔断功能是在请求后端服务失败的数量超过一定比例时,默认是50%, 断路器会切换到开路状态. 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后,默认是5秒, 自动切换到半开路状态.再有新的请求过来时,会尝试将请求发送到后端服务, 如果请求成功, 断路器切换回闭路状态, 否则重新切换到开路状态.所以说Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器还具有自我检测并恢复的能力。
降级的话可以这么理解,我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存.
在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池. 比如将调用的 产品服务 放入A线程池, 调用 账户服务 放入B线程池. 这样做的主要优点是运行环境被隔离开了. 这样就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽时, 不会对系统的其他服务造成影响.
在之前的项目中,我负责过下订单的模块;涉及到订单模块的最主要是订单表以及订单明细表;一个订单对应多个订单明细。我们的订单表中有订单id,会员id,订单总金额,订单中得商品总数,订单状态,订单创建时间,订单发货时间,订单支付时间,订单的收件人等信息;订单明细表中包含订单项id,订单id,会员id,商品id,商品单价,商品数量,商品小计等等。订单的状态有 “未支付,已支付,已发货,交易成功,交易关闭”;当创建订单成功后,订单进入“未支付”状态,支付完成后进入“已支付”状态;收到货物并点击“确认收货后”进入“交易成功”状态;当“取消订单”或者“退货后”进入“交易关闭”状态。我们为了保证订单id的唯一性,当时在项目中是通过 雪花算法,生成的id唯一标识。
具体的业务流程是这样的,当点击“提交订单”后,根据当前登陆的会员,从redis中取出该会员对应的购物车中得信息,将信息插入到订单表以及订单明细表就行了;但这个时候我们要考虑到一个问题,就是在大并发情况下可能出现“超卖”的问题。关于超卖咱们可以这么理解;比如商品A现在的库存只有1个了,如果有两个会员同时来点击提交订单,购买这件商品,那么就会有两个线程同时执行后台的提交订单方法;在我们的提交订单方法中,如果按照常规的处理方案,先查询该商品对应的库存数量,如果库存数量 大于等于 购买数量,则将商品的库存更新为 现有数量减去购买数量。这种常规的处理方法在多线程串行访问的时候不会有啥问题,但在多线程并发访问的时候就有可能产生超卖问题;比如A线程和B线程同时到达了该方法,A线程发现现有库存量为1,用户要购买的商品也是1个,就认为商品足够,然后执行更新商品库存的方法;但在执行更新方法前,B线程也查询了商品库存,发现商品的库存数量也够,所以B线程也会执行后续的更新商品库存的方法,结果A线程将库存数量变成了1-1=0;而B线程又会将库存数量变成0-1 = -1;从而导致商品数量变成了负数,这就是超卖产生的过程。
解决超卖问题的方案有很多,最简单的方案就是在提交订单的方法上通过synchronized加上同步锁,通过同步锁将多个线程的并发访问,变成串行访问,从而解决超卖问题;但如果后期将订单这块单独拆分成微服务,并且进行了集群部署的话,这种单机版的同步锁是不会起到任何作用的。因为单机版的同步锁能够保证单进程多线程的线程安全问题;但不能保证多进程多线程的线程安全问题。要保证多进程多线程的线程安全问题;解决方案有很多。我们项目中 前期 是通过 基于数据库的乐观锁方案来解决的。具体是这么考虑的。改变项目中更新库存的sql语句;
原来的sql语句是 update 表名 set 库存=库存-购买商品的数量 where 商品id=购买的商品的id;
更改后的sql语句 update 表名 set 库存=库存-购买商品的数量 where 商品id=购买的商品的id and 商品库存量>=购买商品的数量;
更改后的sql语句就充分采用了乐观锁特性;说白了就是新追加的where条件 商品库存量>=购买商品的数量,就是乐观锁的体现;在执行update语句的时候,就会对该条记录加行锁;如果有A,B两个线程同时执行同一条记录的更新,则只会有一个线程的更新语句执行;执行后返回的是影响的行数,在我们后端下订单的java代码中,执行对应的这条sql语句,如果返回1则证明执行成功继续后面的业务处理;如果返回0则证明超卖,那么就可以抛出一个自定义的异常;这样既能回滚之前的操作,也能对前台进行相关的提示。这样就能解决 多进程多线程的 线程安全问题。避免超卖问题的产生。
缓存穿透是指查询一个 一定不存在 的数据,比如商品id最多为1000,我查询1001,1002这些一定不存在的id,这个时候因为查询的信息在缓存中不存在,大量的并发请求就会直接到数据库上,而数据库中也不存在这样的数据,所以缓存就形同虚设,这个id一定不存在的请求以后都会到数据库上,增加数据库的压力。
为了解决这个问题,可以缓存空值,也就是说即便在数据库中查询的数据不存在,也会在缓存中缓存个空值,这样下一次就可以走缓存而不是数据库了,但太多的空值也会占用内存空间,导致内存的浪费,可以通过设置过期时间比如3-5分钟,这样当到期后,空值对应的key就会消失;释放空间;但导致的另外一个问题就是如果在这3-5分钟内后台增加数据,这个时候就有id为1001的数据了,而缓存中还是空,所以就会导致内存和缓存中数据的不一致,为了解决这个问题可以在后台增删改数据的同时删除缓存中的数据,从而解决脏数据问题。
这里面还要考虑一个问题,如果用户每次将访问的id都通过随机数去生成一个唯一的值去访问,即便缓存空值也不起作用,这时候就可以进行缓存预热,在启动项目的时候,加载数据库表中的数据到缓存中,这样以后所有的查询走的都是缓存,可以大大减轻数据库的压力,提高项目的性能,也可以避免随机生成id的时候,每次访问数据库。当然在进行相关增删改操作的时候也得删除缓存,避免出现脏数据。
如果缓存在 一段时间内 同时 失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
解决这个问题可以
1.为不同的 key,设置 不同的 过期时间,让缓存失效的时间点尽量均匀
2.做双缓存策略。
A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,
A1缓存失效时间设置为短期,A2设置为长期。
单例:这个对象在内存中只存在一份。调用一个创建对象方法,调用很多次,那么在内存中就只有一份对象。
多例:对象在内存中存在多份。new Brand() ;new Brand()
多线程下的单例设计模式
x1public class Singleton2 {
2
3private static Singleton2 singleton2 = null;
4
5private Singleton2() {
6
7}
8
9public static Singleton2 getInstance() {
10if (singleton2 == null) {
11synchronized (Singleton2.class) {
12if (singleton2 == null) {
13singleton2 = new Singleton2();
14}
15}
16}
17return singleton2;
18}
19}
小伙,你对单例设计模式是怎么理解
单例我是这样理解的,说白了就是为了保证类的实例对象在内存中只有一份。为了实现这么个效果,我们需要通过单例来完成。写单例的时候有那么几个注意点;首先我们要把构造函数给私有化,这样做其实就是为了避免,在外部通过new来创建多份对象;再者我们会有一个私有的静态的全局变量,并且把它的初始值设置为null,还有要写一个公共的静态的方法来让外部去调用,在这个方法中我们会进行相关的判断,如果静态的全局变量为null,那么就通过new实例化;然后给全局变量赋值,之后返回就行了,如果不为空,就直接返回它,这样就能保证多次调用这个方法时,只有第一次去创建一个新的对象,后面再访问的时候,就直接返回之前创建的对象就行了,这样也就保证了单例。
这样做在单线程下是没问题的;但是在多线程并发访问的时候,如果多次调用这个方法,就有可能创建多个实例对象;为了解决 多线程访问下 单例设计模式的 线程安全问题,我们可以直接在方法上加上一个synchronized同步锁来进行解决。
但是通过在方法上加上同步锁来解决,在性能上比较低;因为每个线程在调用方法前,都需要等待获取锁,等到在它之前的线程执行完并且释放锁后,这个线程才能获取锁,执行相关的逻辑。所以这个性能低下,主要是体现在等待以及加锁,解锁的过程。
我们就可以通过双重判定锁来解决这个性能低下的问题。打个比方吧,如果有A,B两个线程同时访问这个方法,因为并没有在方法上加锁,所以A,B两个线程可以同时进入第一层判断,然后发现对象为空,所以就执行下面的同步代码块,这时候比如A获取到这个锁,那么B就得在同步代码块外等待,A获取锁后呢,进入到第二层判断了,发现对象还是空,所以就会创建这个类的实例化对象并且给全局变量赋值,之后返回这个新创建的对象;A线程执行完毕,释放锁,B线程进入这个同步代码块中,发现对象已经不为空,那么就直接返回之前创建的那个对象就行了;后面来的所有的其它线程再进入第一层判断的时候,发现对象不为空,那么就直接返回之前创建的那个对象,这样也就不会再去等待以及加锁,解锁了。这样即实现了单例的效果,也提高了性能。
通过双重判定锁,可以提高性能;但是这里面还存在一个指令重排序的问题;说白了就是在正常情况下,当你去new一个类的实例化对象时,它要经过这几步,首先在堆中开辟一块内存空间,之后呢就创建类的实例化对象并放到这个内存空间中,最后通过指针去指向这个空间中得对象。但是jvm会对代码进行优化,把之前的,1-2-3这个顺序变成了1-3-2;这时候在多线程并发访问下,就会出现这么一个问题,比如A线程进入到双重判断锁的第二层判断,发现对象还是空,所以就会通过new去实例化类的对象;但它的执行顺序可能是先开辟内存空间,然后就直接让指针指向了这个内存空间,这时候内存中还没有对象呢,又来了个B线程,在执行双重判断锁的第一层判断的时候,发现对象已经不为空了,就直接返回了这个错误的对象,在后续的使用过程中肯定就会出现问题。为了避免指令重排序的问题,我们可以通过Volatile关键字来解决。
public class Thread1 implements Runnable {
public void run() {
}
}
public class Main1 {
public static void main(String[] args) {
Thread t1 = new Thread(new Thread1());
t1.start();
}
}
mysql分页:
每页有3条记录,查询出第3页的数据?
分析:mysql的分页是 limit 开始位置,每页条数;开始位置是从0开始的;0对应的是第1条记录
开始位置=(当前页-1) * 每页条数 也就是 (3-1) * 3 = 6;
所以sql语句为
select t.* from (select * from 表名) t limit 6,3;
如果要求变成:查询销量小于100,价格在2000-3000之间的商品,并按照销量进行降序排列,每页有3条记录,查询出第3页的数据?
select t.* from (select * from t_product where 销量<100 and price >= 2000 and price <= 3000 order by 销量 desc) t
limit 6,3;
oracle分页:
每页有3条记录,查询出第3页的数据?
分析:oracle的分页关键字是rownum; rownum是从1开始的;
开始位置=(当前页-1) * 每页条数 + 1; 也就是 (3-1) * 3 +1 = 7
结束位置= 当前页 * 每页条数; 也就是 3 * 3 = 9
所以sql语句为
select * from
(select t.*,rownum rn from
(select * from 表名) t
where rownum <= 9) where rn >= 7
如果要求变成:查询销量小于100,价格在2000-3000之间的商品,并按照销量进行降序排列,每页有3条记录,查询出第3页的数据?
select * from
(select t.*,rownum rn from
(
select * from t_product
where 销量<100 and price >= 2000 and price <= 3000
order by 销量 desc
) t
where rownum <= 9) where rn >= 7
从下到上分别是:
物理层
数据链路层
网络层
传输层
会话层
表示层
应用层
xxxxxxxxxx
311. #将传入的数据根据类型进行相应的转换,如果类型不匹配则报错。如果传入的是字符串类型则会自动加上双引号。
22. $将传入的数据直接显示在sql中。
33. #方式能够很大程度上防止sql注入,$方式无法防止Sql注入,所以一般能用#的就别用$。
在我们的项目中目前还没有遇到分库分表,不过在平时上网查资料的过程中,我也会看到一些,我的理解是这样的:
当单张表的数据量特别大时,可以考虑采用分库分表,来提高性能。
可以进行垂直拆分或者水平拆分。
垂直分库,说白了就是根据不同的业务功能模块将相关的表单独放到一个数据库中。比如 可以将订单相关的表都放到一个数据库中;将产品相关的表也单独放到一个数据库中。
垂直分表,说白了就是当一张表的字段特别多时,将字段拆分到不同的多张表中。 比如一张表有100个字段,那么将这个100个字段拆分到5张或者10张表中,从而减少一个表的大小。
水平切分的话,比如可以按照 数值范围切分:根据id将1-9999的数据放到第一个库中, 将10000-20000的数据放到第二个库中,以此类推;或者根据日期将某年的数据或者某月的数据 放到不同的库中。
参考文章:
https://mp.weixin.qq.com/s/sntOz-hg-3c5TdZCFvYIjg
https://mp.weixin.qq.com/s/sX4DnACgyJG5NIfgJ-3Q8w
参考代码:
https://gitee.com/li_haodong/SpringBootLearn/tree/master/spring-boot-sharding-table
分布式事务这块我是这样理解的,当操作涉及到多个数据库或者涉及到微服务之间的调用,就会产生分布式事务。
在平时上网查资料的过程中,我看到过阿里巴巴分布式事务框架seata,它的工作流程大概是这个样子:
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
RM 向 TC 注册分支事务,将这个分支事务纳入 XID 对应全局事务的管辖;
TM 向 TC 发起针对 XID 的全局提交或回滚;
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
就说我们项目中没用过,我们用的是 springCloud 和 springBoot
就说我们项目中是使用eureka来充当注册中心的。
就说我们项目中使用的是consul
Zuul也好,Feign也好,在springCloud中之所以能够实现负载均衡,本质上靠的都是Ribbon。
xxxxxxxxxx
61HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。默认构建一个初始容量为 16,负载因子为 0.75 的 HashMap。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
2
3在调用put(key,value)向hashmap中存值得时候,会根据key的hashcode得到一个数值,比如这个值为1,那么会用这个值和数组长度取模求余数,得到要存放到数组中得下标位置,然后看这个位置上是否为空,如果为空,则直接将这个键值对存储到这个位置,如果不会空,则存储到该数组位置后面对应的链表中。这样就解决了hash冲突的问题。
4
5在调用get(key)来获取值得时候,同样计算key的hashcode值,将得到的值和数组的长度取模求余数,从而得到下标,根据下标,找到数组中具体的位置,然后再调用equals方法,将key和该位置的key进行对比,如果相同,就获取该位置的值,如果不同,则去该位置后面的链表中一个个的进行对比,如果找到相同的Key,就返回这个位置的值,如果还找不到则返回空。
6为了提高查询的速度,在Jdk1.8引入了红黑树,目的就是为了避免单条链表过长而影响查询效率。
线程安全的hashmap。当有多个线程同时去操作一个hashMap,就会引发线程安全问题。如果通过synchronized去保证hashmap的线程安全当然是可以的,但相对来说比较耗费性能,所以可以通过concurrentHashMap来保证多线程操作时候的线程安全。
concurrentHashMap的本质就是采用分段锁,将一个大数组,划分程多段小数组,对每段小数组加锁,这样就可以提高并发性。比如两个key,经过hash取模运算被放到了不同的小段数组中,那么这两个操作就都能同时进行,避免了因为锁定整个map而导致的串行化,从而可以提高性能。
在jdk1.8之后,又做了优化,进行了锁粒度的细化,会对数组中的每个元素加上不同的锁。具体做的时候是这样子,当有两个线程同时在数组中的同一个位置进行put操作,那么就会通过CAS保证在同一时间内只有一个线程会执行操作,CAS的过程就是先拿到数组中这个位置的值,之后在设置新值时,先比较下之前拿到的值和现在的值是否相等,如果相等就进行更新和替换。之后其他线程过来了发现这个位置的值和之前拿到的值不一样,证明这个位置已经被其他线程给放进去值了,那么就会通过synchronized把这个位置给锁住,然后将这个键值对,存入链表或者红黑树这样在同一时间,就可以有多个线程对数组中不同的位置进行操作,从而在保证线程安全的情况下,更大的提高并发性。
我们从springboot项目的启动类中可以看到最核心的两行代码, @SpringBootApplication和SpringApplication.run方法。
在 @SpringBootApplication 的内部包含了3个注解
@Configuration @EnableAutoConfiguration @ComponentScan
@Configuration是基于JavaConfig形式的Spring Ioc容器的配置类,可以把它看成xml配置文件中的beans标签。@Configuration写到类上面,在类中的方法上如果写了 @Bean注解,那么它的返回值将作为一个bean注册到Spring的IoC容器中,方法名默认作为bean的id。
@ComponentScan这个注解对应XML配置中的context:component-scan元素,说白了它的作用就是自动扫描并加载符合条件的组件,比如 @Component和 @Service,@Controller等,最终将这些bean定义加载到IoC容器中。
我们可以通过basePackages来指定 @ComponentScan 自动扫描的范围,如果不指定,则默认情况下SpringBoot框架会扫描启动类所在包下面所有子包的所有类。这也是SpringBoot的启动类最好是放在root package(最顶级包)下的原因。
@EnableAutoConfiguration 这个注解是借助 @Import的帮助,将所有符合自动配置条件的bean定义加载到IoC容器中。
整个处理过程从一个HTTP请求开始:
1.Tomcat在启动时加载解析web.xml,找到spring mvc的前端总控制器DispatcherServlet,并且通过DispatcherServlet来加载相关的配置文件信息。
2.DispatcherServlet接收到客户端请求,找到对应HandlerMapping,根据映射规则,找到对应的处理器(Handler)。
3.调用相应处理器中的处理方法,处理该请求后,会返回一个ModelAndView。
4.DispatcherServlet根据得到的ModelAndView中的视图对象,找到一个合适的ViewResolver(视图解析器),根据视图解析器的配置,
DispatcherServlet将要显示的数据传给对应的视图,最后显示给用户。
1.需求分析(通过需求分析最终写一个 需求分析的文档)
2.概要设计 (最终写一个 概要设计文档)
3.详细设计(用例图,流程图,类图)(最终写一个详细设计文档)
4.数据库设计(powerdesigner) (最终写一个数据库设计文档)
5.代码开发(编写)
6.单元测试(junit 白盒测试)(开发人员)
7.集成测试 (黑盒测试/功能测试,loadrunner(编写测试脚本)(高级测试))(测试人员)
8.上线试运行 (用户自己体验)
9.压力测试(jmeter压力测试工具)
10.正式上线
11.维护
1.Map是一个以键值对存储的接口。Map下有两个具体的实现,分别是HashMap和HashTable.
2.HashMap是线程非安全的,HashTable是线程安全的,所以HashMap的效率高于HashTable.
3.HashMap允许键或值为空,而HashTable不允许键或值为空
4.HashTable之所以是线程安全的是因为在它的方法上加上了synchronized锁/同步锁。
5.HashMap是无序的(存值时候的顺序和遍历取值时候的顺序不一致),LinkedHashMap是有序的
List和Set都是接口,他们都继承于接口Collection,List是一个有序的可重复的集合,而Set的无序的不可重复的集合。
Collection是集合的顶层接口,Collections是一个封装了众多关于集合操作的静态方法的工具类,因为构造方法是私有的,所以不能实例化。
List接口实现类有ArrayList,LinkedList,Vector。ArrayList和Vector是基于数组实现的,所以查询的时候速度快,而在进行增加和删除的时候速度较慢LinkedList是基于链式存储结构,所以在进行查询的时候速度较慢但在进行增加和删除的时候速度较快。又因为Vector是线程安全的,所以他和ArrayList相比而言,查询效率要低。
String是一个常量,是不可变的,所以对于每一次+=赋值都会创建一个新的对象,StringBuffer和StringBuilder都是可变的,当进行字符串拼接时采用append方法,在原来的基础上进行追加,所以性能比String要高,又因为StringBuffer是线程安全的而StringBuilder是线程非安全的,所以StringBuilder的效率高于StringBuffer.对于大数据量的字符串的拼接,采用StringBuffer,StringBuilder。
1.一个类只能进行单继承,但可以实现多个接口。
2.有抽象方法的类一定是抽象类,但是抽象类里面不一定有抽象方法;
接口里面所有的方法的默认修饰符为public abstract,接口里的成员变量默认的修饰符为 pulbic static final。
关系:
接口和接口 继承
接口和抽象类 抽象类实现接口
类和抽象类 类继承抽象类
类和类 继承
重载:重载发生在同一个类中,在该类中如果存在多个同名方法,但是方法的参数类型和个数不一样,那么说明该方法被重载了。
重写:重写发生在子类继承父类的关系中,父类中的方法被子类继承,方法名,返回值类型,参数完全一样,但是方法体不一样,那么说明父类中的该方法被子类重写了。
面向对象编程有四个特征:抽象,封装,继承,多态。
多态有四种体现形式:
其中重载和重写为核心。
重载:重载发生在同一个类中,在该类中如果存在多个同名方法,但是方法的参数类型和个数不一样,那么说明该方法被重载了。
重写:重写发生在子类继承父类的关系中,父类中的方法被子类继承,方法名,返回值类型,参数完全一样,但是方法体不一样,那么说明父类中的该方法被子类重写了。
咱们都知道Spring的核心就是IOC和AOP这两大特性,IOC是基于工厂设计模式,我的理解就是说原来咱们想要创建一个类的对象,得自己通过new的方式进行,而现在可以通过在spring的配置文件中写上一段
AOP是基于代理设计模式,代理分为动态代理和静态代理,在项目中默认使用的是基于jdk的动态代理,它需要接口的支持,这就是在写service业务逻辑层的时候通常先写接口,再写实现类的原因,如果没有接口只有类,这个时候可以使用CgLib这个动态代理来实现。AOP说白了就是面向切面编程,就是把一些非核心业务逻辑提取出来形成一个切面,从而让咱们程序员在编写代码时候,只用关注核心业务逻辑的处理,至于这些非核心业务逻辑统一在切面里面处理。像日志记录,性能统计,事务处理,安全控制,这些都可以通过AOP的方式进行统一的处理。切面说的简单点就是在 指定类的指定方法 的 前后 执行特定的横切业务逻辑。切面 由切点和通知构成,通知又包括方位和横切业务逻辑,切点就是为了定位指定类的指定方法,方位包括 前置通知,后置通知,环绕通知,抛出异常通知,返回后通知 这些;横切业务逻辑说白了就是一些公共的非核心业务代码。
AOP的切面在项目中用的还是比较多的,咱们都知道事务管理就是通过AOP切面的方式来实现的。切面的好处就是把分散在代码中的重复代码提取出来进行统一的维护和控制。我在项目中就使用aop完成日志的统一处理。原来是把日志记录的相关代码分散到各个控制层的相关方法中,但这就会导致程序员在开发时候不能将精力集中到业务逻辑的处理上,还得考虑记录日志,工作效率就大打折扣,我在项目中负责过日志管理模块,通过....[可以将基于AOP的日志统一处理融进去]
平时在项目中都是用SpringAOP来进行本地事务的控制,这样可以将事务控制的代码提取出来成为一个切面,减少重复代码,让我们在编程时将精力花在核心业务逻辑的处理上,不用再去操心开启事务,提交事务,回滚事务这些操作,因为这些操作都通过springAOP帮我们实现了。
我们通常都是对service层中的 增删改 操作进行事务的控制,需要配置事务的传播特性为propagation="required",把增,删,改以外的操作,也就是查询操作,配置成只读事务read-only="true",这样以提高性能。
默认情况下事务只对运行时异常进行回滚,可以通过rollback-for="Exception"使其对所有异常都进行回滚。
咱们都知道事务有四大特性,也就A:原子性,C:一致性,I:隔离性,D:持久性。
原子性是说把多个操作看成一个不可分割的整体,要么都成功,要么都失败。
一致性是说事务能保证数据从一个 一致性状态 到 另外一个 一致性状态,数据不被破坏。
隔离性是指一个事务的执行不被其他事务干扰。
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的。
事务的 传播特性 有7种,最常用的就是 propagation="required",我们在项目中用的就是这个。它的意思是如果当前没有事务,就新建一个事务,如果已经存在一个事务,就加入到这个事务中。比如在ServiceA中的一个事务方法A中调用了ServiceB中的一个事务方法B,如果propagation="required",就代表这两个方法都在一个事务内,那么ServiceB中的方法抛出异常了,也会导致ServiceA中的事务方法回滚。
其余的几个记得不是太清楚了,好像还有propagation="SUPPORTS",propagation="REQUIRES_NEW"。
事务的隔离级别有四种,分别是 读未提交,读提交,重复读和串行化。
读 未提交:
就是A事务可以读取B事务未提交的数据,这样会产生脏读。
读 提交:
就是A事务要等B事务提交后才能读取数据。可以解决脏读的问题,但会出现不可重复读的问题。
可重复读:
通过把事务的隔离级别设置为 可重复读,就可以解决 不可重复读 的问题,Mysql 默认的事务隔离级别就是 可重复读。先说下 不可重复读,比如A事务访问了id=1的商品,它的库存量是1000,在A事务还没有结束时,如果B事务也去访问id=1的商品,并将商品的库存量更新为10,并提交事务;这时候A又读取id=1的商品时,就发现它的库存量是10了,这样在A事务范围内,就出现了id=1的商品,两次读取的库存量不一样,这就是不可重复读。通过设置为重复读就可以解决这个问题,但这时又会产生 幻读的问题。幻读可以这样理解,比如A事务查询id<10的条数有2条,当A事务还没有结束的时候,B事务又插入了一条数据,并提交事务,这时A再读取id<10的数据,发现有3条了,这就是幻读。可以将事务的 隔离级别 设置为 串行化,就能解决 幻读的问题。
串行化:
是最高的事务隔离级别,在这个级别下,事务 会 串行化 顺序执行,可以避免脏读、不可重复读与幻读。但是它的性能特别低,所以一般不会使用。
我们为了提高项目的性能,在sql语句这块会结合索引进行一定的调优。
在项目中我们用的比较多的就是普通索引和唯一索引;也会根据业务的情况使用组合索引。咱们都知道表里面的主键会自动生成一个唯一索引;唯一索引的特点就是,在它所在的列上不能有重复的值;普通索引所在的列上可以有重复的值。索引可以提高查询的性能,但是对于 增,删,改这些方面来说,还要进行额外的维护操作,而且索引越多也要占用更多的额外的空间;所以我们可以结合业务的情况,将多个字段组合起来设置为组合索引;这样就既可以提高查询的性能,也不会因为索引太多,占用额外的空间而导致性能损耗,比如在项目的商品表中,我们有分类1,分类2,分类3;这时候我们可以将这多个字段组成一个组合索引;组合索引在使用的时候要遵循最左匹配原则,说白了就是建立索引的时候,如果顺序是A,B,C;那么在使用这个组合索引的时候,在where条件后,查询的顺序是A,AB,ABC以及AC都是可以的,但如果跳过了A,直接是B或者BC那么索引就不会起作用,因为不符合最左匹配原则。
当然我们在创建索引的时候要遵循一些原则,比如:
给频繁查询的字段上创建索引
order by,group by后的字段,以及外键加索引
根据当前字段的业务含义,来区分是创建唯一索引还是创建普通索引
考虑到索引不是越多越好,也可以根据业务情况创建复合索引,想要使其 复合索引 起作用,在查询的时候需要按照当时创建复合索引的字段的顺序来,也就是最左匹配原则。
再者我们可以根据执行计划来看sql语句具体使用索引的情况,在mysql中可以通过explain来查看。
这里面会涉及到一些关键点:
key:代表mysql实际使用的索引名。
type:这个字段比较重要,它表示MySQL在表中找到行的方式:
Const:唯一索引对应的字段在where条件中
Range:范围扫描,常见于between、<、>等的查询
Index:代表扫描 索引树
ALL: 代表全表扫描,不走索引。
为了避免索引失效,我们在写sql语句的时候也要注意些,比如:
不要在有索引的字段上进行运算,否则索引会失效,会进行全表扫描
在写like模糊匹配时,不要在前面加%,否则索引失效
再者就是再写sql语句的时候,为了能够达到更好的性能,我们需要这么做:
外键必须加索引,这样可以提升多表联查的性能。
通过冗余字段避免多表连查,从而提高性能。比如在商品表中可以不仅有分类的id,还可以加上分类的名字,这样就不用为了显示分类名字而去进行多表联查了。又比如在会员表中不仅有地区的id,还可以有地区的名字这样也可以避免为了显示地区名而进行多表联查。
SELECT语句中避免使用'*’,只查询需要返回的字段,这样可以减少解析sql语句的时间,以及减少 带宽,cpu,内存,io等 各方面的消耗。
通常要用小表去驱动大表,这样可以提高性能,在left join 中 左边的表为驱动表,所以应该让左边的表尽可能的为小表,在right join 中 右边的表为驱动表,所以应该让右边的表尽可能的小,在 inner join 中 mysql会自动选择较小的表为驱动表。
1.启用线程池,默认的tomcat没有启用线程池,【修改server.xml】
在tomcat中每一个用户请求都是一个线程,所以可以使用线程池提高性能。
我们在项目中将maxThreads[最大线程数]设置为1000,将排队数acceptCount和maxThreads设置相等。
当tomcat的线程数达到maxThreads后,新的请求就会排队等待,超过排队数的请求会被拒绝。
2.使用64位的tomcat和jdk,禁用AJP协议。【修改server.xml】
3.开启APR通讯模式,支持高并发。因为默认tomcat采用的是性能最低的BIO【阻塞IO】模式。【修改server.xml】
4.tomcat中设置JVM参数 通过-server开启server模式 通过-Xms3000/2000/4000m和-Xmx3000/2000/4000m设置初始堆大小和最大堆大小,通常将两个值设置为一样,避免堆空间不断增大和缩小所带来的性能损耗。[修改catalina.sh]
方法区【持久代】 堆【java对象】 栈【基本数据类型,对象引用(指针/对象名)】 本地方法栈 计数器
我们的linux服务器,安装centos6.5/ centos7.0, 64位的操作系统和64位的软件,配置了4/8个cpu,32G/64G/128G内存。
我们在jvm优化的时候是这样做的 首先JVM将内存划分为: 年轻代 年老代 永久代(方法区) 其中年轻代和年老代属于堆内存,永久代不属于堆内存,由虚拟机直接分配。 年轻代:年轻代用来存放JVM刚分配的Java对象 年老代:年轻代中经过垃圾回收没有回收掉的对象将被放到年老代 永久代:永久代存放Class类、Method方法 这些元数据信息,它的大小 跟 项目的规模以及类和方法的数量有关,一般设置为128M/256M就足够,预留30%的空间。通过-XX:PermSize=128M -XX:MaxPermSize=128M 来设置永久代(方法区)的大小。
jvm的垃圾回收算法有[GC算法] 串行算法(单线程) 并行算法 并发算法 吞吐量优先的并行收集器 响应时间优先的并发收集器
xxxxxxxxxx
31我们的项目设置的是响应时间优先的并发收集器,
2将堆大小通过 -Xms -Xmx设置为3-5G,将年轻代通过 -Xmn 设置为2g,
3设置年老代为并发收集,当时设置的是运行6-8次GC以后对 内存空间进行压缩、整理。打开对年老代的压缩,可以消除碎片。
我们在项目中也会通过线程池的方式来提高程序的性能。
首先线程池可以避免频繁的创建和销毁线程所造成的性能损耗,再者用线程池可以提高 处理大批量数据的 性能,节省时间,比如我要将项目中的所有图片加水印,这时候就可以使用线程池。就像要洗100个碗,你可以让一个人去干,这一个人就是一个线程,你也可以让10个人一块去干,这就相当于是线程池中的多线程。相比而言多线程执行的时间更短,效率更高。
在项目中我们是用ThreadPoolExecutor来创建线程池的,它里面有几个核心的参数信息,线程池的核心大小,队列以及线程池的最大值,还有线程的存活时间。线程池的工作原理是这样的,默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。如果当前线程池中的线程数目小于核心线程数,那么每来一个任务,就会创建一个线程去执行这个任务;如果当前线程池中的线程数量已经达到了核心线程数,则每来一个任务,就会尝试将它添加到队列中,如果添加成功,则该任务会等待空闲线程将其取出去 执行;如果队列已经满了,那么就会添加失败,就会尝试创建新的线程去执行这个任务;这时候创建新线程依据的是线程池的最大线程数,如果当前线程池中的线程数量达到了最大线程数,则会采取拒绝策略进行处理;说的简单点就是先把核心线程数给占满了,不够用就开始往队列里面放,如果队列也占满了,就继续创建更多的线程来处理任务,如果创建的线程的数量达到了线程池中得最大线程数,再有新的任务过来,就可以根据策略进行拒绝处理。
如果线程池中的线程数量大于 corePoolSize【核心线程数】时,某个线程的空闲时间超过了keepAliveTime【存活时间】,线程就会被终止,直到线程池中的线程数量不大于corePoolSize【核心线程数】。
线程池中的队列,一般常用的有ArrayBlockingQueue【有界阻塞队列】:它是基于数组的队列,创建时必须指定大小;
还有LinkedBlockingQueue【无界阻塞队列】:它是基于链表的队列,一般不用指定大小,如果创建时没有指定大小,默认值是Integer.MAX_VALUE;
实现线程有两种方式,一种是继承于Thread类,一种是实现Runnable接口。通常情况下都使用实现Runnable接口。
默认情况下,main方法会有两个线程,一个是主(main)线程,一个是垃圾回收线程。线程是异步执行的。
主线程执行完毕后,jvm不会结束,而是要等相关的子线程也执行完毕,jvm才会结束。
当多个线程访问同一个共享资源;如果执行的结果和预期结果一致则证明线程安全,否则就证明线程不安全。
产生线程安全问题的两个因素:
多个线程
操作同一个资源
多个人上厕所:
每个人是一个线程
厕所里面的一个坑是一个资源
多个人去试衣间:
多个人去ATM取钱:
如何解决线程安全问题:
通过加锁解决线程安全的问题。【加锁--解锁,哪个线程抢到那把锁,那么它就先执行,执行完后释放锁,其他线程再去获取锁】
线程的状态:
新建状态:调用了new方法后
就绪状态:调用了start方法后
运行状态:获取了cpu的使用权,执行run方法
阻塞状态:线程处于放弃cpu使用权,处于停止状态
等待阻塞:wait方法
同步阻塞:synchronized
其他阻塞:sleep方法
死亡状态:run方法执行完毕后,或者执行过程中抛出了异常退出了run方法。
在多线程中如果涉及到共享资源的访问,就会引发线程安全问题,说白了就是执行的结果和预期的结果不一致。遇到多线程并发访问的线程安全问题,我们通常可以采用锁的方式进行解决。
最经常用的就是synchronized同步锁,我在写单例设计模式的时候就会用到。 synchronized同步锁,在线程进入方法时自动加锁,当方法执行完毕后自动解锁。比如有A,B两个线程同时访问一个带有同步锁的方法,则同一时刻,只有一个线程能够获取锁,进入到方法中;如果A线程获取了锁,那么B线程就得等待,等到A线程执行方法完毕后,会自动释放锁,这时候B线程再获取锁,进入到方法中执行。所以synchronized同步锁,能够将多线程的并发访问,变成一个一个的串行访问。
像java里面的hashtable和vector都是用的synchronized来保证线程安全。
除此之外,还有读写锁。一个是读操作相关的锁,也叫共享锁;另一个是写操作相关的锁,也叫排它锁。
多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。
通过分离读锁和写锁,使得并发性相比一般的排它锁有了很大的提升。也就是说多个线程可以同时进行 读取操作,但是同一时刻 只允许一个线程进行 写入操作。通过lock()方法加锁,通过unlock()方法解锁。
除此之外还有分布式锁,普通的锁[synchronized/单机版]是为了解决多线程访问产生的安全问题,分布式锁是为了解决多进程【多个tomcat,多个jvm】多线程访问产生的安全问题。我们在项目中可以使用Redisson来完成分布式锁。
除此之外从数据库层面来说有悲观锁,乐观锁以及表锁和行锁。
悲观锁:
每次去查询数据的时候都认为别人会修改,
所以每次在查询数据的时候都会上锁,
这样别人想拿这个数据就会阻塞直到它拿到锁。
传统的关系型数据库里边就用到了这种锁机制,
比如通过select ....for update进行数据锁定。
乐观锁:
每次去查询数据的时候都认为别人不会修改,
所以不会上锁,但是在提交的时候会判断一下在此期间别人有没有
去更新这个数据,可以使用版本号,时间戳等机制;
乐观锁适用于多读的应用类型,这样可以提高吞吐量。
有AOF和RDB两种方式,我们一般使用默认的RDB方式。因为它的效率会更高,RDB持久化是在指定的 时间 间隔内 将内存中的 数据 写入到硬盘的rdb文件中,实际操作过程是fork[分出]一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用 二进制 压缩存储。具体是在redis.conf中配置的,找到save关键字,指定 每隔 多长时间 有 多少个key 发生变化,就将其持久化。
AOF:是将 每个 写操作对应的指令 保存到后缀名为 aof的 文本 文件中。
rdb的性能要高于aof: 因为rdb中直接存储的是数据,所以在redis服务器启动的时候,直接加载rdb文件中的数据到内存中就行了。
而aof中存储的是写指令,所以在redis服务器启动的时候,需要先加载aof文件中的指令,之后还需要再次执行这些指令。
aof的 实时性、可靠性 要高于rdb: 因为每个写操作指令都会 及时 写入aof文件中,所以aof这种持久化策略不容易丢失数据,而rdb是在指定的时间间隔后才会写入数据到文件中,所以有可能会丢失一部分数据。
了解部分: save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,就会持久化。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,就会持久化。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,就会持久化。
dbfilename dump.rdb
save 900 1 save 300 10 save 60 10000
Docker可以理解为一种容器化技术。说白了我们不使用docker也能在linux上部署安装和使用各种软件;但这种常规的方式会导致安装配置过程麻烦,而且好不容易安装配置好了,如果要迁移到一台新的linux服务器上,就需要再重新做一遍,这样就特别浪费时间;使用docker后,可以进行一键部署,刚才提到的那些问题也能很方便的解决。
Docker中比较重要的就是注册中心,镜像,容器这三部分。就拿我平时工作中用到的来说吧,部署项目并通过nginx+tomcat进行负载均衡;如果使用常规的方式来做,既浪费时间又麻烦;使用docker后,先通过docker pull从注册中心中下载nginx和tomcat的镜像;之后可以通过docker images查看本地下载过来的镜像,然后通过docker run -d 镜像id 来启动镜像,运行后的镜像就是容器;可以通过一份镜像来启动多个容器,比如通过同一个tomcat镜像可以运行多个tomcat容器;通过docker ps 查看启动的容器;通过docker exec -it 容器id /bin/bash 来进入到容器内部,在启动镜像的时候可以通过 -p 来指定端口映射,从而将容器内的端口映射到本地主机上,这样就可以进行访问了。
通过 netstat -tlanp | grep 进程名 查看进程占用的端口号
通过 service iptables restart 重启防火墙
通过 ps -ef | grep 进程名 查看指定进程的进程号
通过 kill -9 进程号 杀死进程
通过 wget 在线下载指定的文件
通过 yum 进行软件的在线安装
通过 make 和 make install 基于源码进行软件的编译,安装
通过 find -name 文件名 进行文件的查找
通过 df -h 查看硬盘空间
通过 top 查看系统的负载情况
通过 free 查看内存大小
jbpm,Activiti
啥是工作流:
邮箱不要用qq,用163,126都行
4年工作经验写5个项目,项目按照时间倒序排列
最开始的前2个项目最重要,时间段在10-13个月之间,时间随意选,差异化;后面的3个项目 5-8 月之间
如果是外网项目,必须能查到,要选3,4线的互联网项目,也可以是手机APP,选那些下载量少的,用的少的;
如果是内网项目,建议 中字 打头;中石化,中石油;中国电信,中国联通,铁路,财政局等等
医院APP,保险APP,购物APP,商城APP
技能专长:
xxxxxxxxxx
161
2SpringBoot SpringMVC Spring MyBatis MP(MyBatis-Plus) HttpClient
3
4SpringCloud(Feign Eureka注册中心 Zuul网关 Hystrix Consul注册中心、配置中心 Gateway网关)
5
6接口安全 接口限流 Yapi Swagger 基于JWT的登录 微信支付/支付宝支付 阿里云OSS 网易云信 订单
7
8Vue全家桶(Vue Vuex VueRouter Axios) Echarts Ajax跨域 Bootstrap
9
10消息中间件RabbitMQ Redis分布式缓存 MinIO分布式文件系统 MongoDB 线程池
11
12Nginx+Tomcat负载均衡 KeepAlived高可用 Linux MySQL主从复制 SQL优化 Tomcat调优
13
14Maven SVN Git Docker PostMan Jmeter Junit单元测试
15
16AOP日志统一处理 反射导出Excel Word Pdf 定时器Quartz/Spring Task
业务功能模块:
电商后台:
订单管理
会员管理
系统管理
电商前台:
你上家公司在哪?【北京市海淀区上地五街华悦大厦11层1108】
你在哪住?【精确到小区名】【每个人都要准备两套小区,一套是在郑州;一套和你简历上相关的】
你去公司怎么坐车?【之前的两家老公司是怎么坐车的 1号线--》xxx站--》倒公交xx路】
你期望薪资多少?【15k;16k】[8-11k]
你上家工资多少?【最多差1000,14k,15k】
税后拿到手的有多少?[差1000左右,13k]
扣了多少钱的税?[1000左右 模糊词]
你哪个学校毕业的?【学校名 简称】
学的什么专业?【】
你们学校还有啥其他专业吗?【你得了解学校】【3-4个左右】
你大学都学了什么?【专业课 计算机原理 数据库原理 操作系统原理 数据结构和算法】
你今年多大了?【26】【哪年】【身份证,1999,年月日;】
学历证上,是真实的身份证号
到公司,只有真正签合同的时候,才填写 真实的身份证号【】
除此之外,都是咱们自己 编的 身份证号【应聘登记表】
属相是啥?【猪】
你为啥从上家公司离职?
1.想换一个新的环境,公司这边也挽留我,还想换一个新的平台,来不断提升充实自己。
2.之前在外包公司,最近公司接的项目比较少,想换一个新的平台去充实提升自己。
3.公司最近转型,转向.net/php了,java这方面的项目也少了,所以想换个环境,不断提高充实自己。
4.上家公司的福利待遇也算不错和同事相处的也蛮开心的,但是在技术提升方面不太符合自己发展的预期。
你为啥要回来的?
父母年龄大了,想离他们近点,也方便照顾;
你交社保了吗【五险一金】?【没有】为啥没交【不交的是非法的】?
养老保险,医疗保险,失业保险,工伤保险,生育险,住房公积金(三险一金,没有一金,不给上)
为啥没交:
在上家公司的时候,人事说咱们公司的五险一金如果要上的话都是从自己工资里面扣的【全部从你工资里面扣】,当时感觉没啥必要也就没上。
入职面谈的时候,说可以给上,也可以折现,我当时觉得还是折现好些,所以也就没上【折现了多少钱1000左右】。
你说你以前没上过,怎么我去给你缴纳五险一金的时候,信息显示你上过呢?
这个我也不太清楚。
五险一金上面的缴纳公司怎么和你说的公司名不一样呢?
这个我也不清楚,公司可能有它自己的方式吧。
你的优缺点是啥?
优点:2-3词
善于倾听,认真细致,团队协作能力强,抗压能力强,乐于分享,喜欢钻研新技术
缺点:
1.我这个人说话比较直,在和团队成员探讨问题的时候,容易得罪人,不过现在已经改进的差不多了。
2.研究技术时候,想把这个技术搞的很清楚,结果才开始的时候影响了项目开发的进度,我也意识到这个问题,所以后来对于我比较感兴趣的技术,我 会利用自己的业余时间去钻研它,在工作时以项目任务为主。
3.这几年做程序,因为要不断的对代码进行验证确认,所有感觉自己现在有点强迫症。
你的 五/三 年规划是啥?
前期继续加强自己的技术功底,然后朝着项目经理、技术经理、产品经理 方面发展
你啥时候上的大学?
哪年毕业的?
从火车站怎么到你们学校?
你们学校周围都有啥?
你们公司有多少人?【模糊词 60多人,70,80号;100左右】
你们公司有哪几个部门?
行政部
财务部
人事部
技术部【30】
项目组有多少人【6-10】
几个项目组 【3】
销售部
你是统招吗:是
学信网上能查吗:我的可以在民教网上查
能加班吗:能
能接受出差吗:可以
应聘登记表:
留个微信,加个微信:
面试中得反问。
先苦后甜,先吃苦后享福。
养成做日计划的习惯:
要把问题说清楚:
要学会汇报工作:【主动汇报,而不是问一答一】
表达的内容:
差异化【你可以颠倒顺序】,你搞懂要表达的核心点,颠倒顺序。
说的时候可以丢东西,但不能丢核心点。短的必须说出来,长的可以省略以部分,说出关键点。
表达注意事项:
眼神交流。
面部表情。【多有点笑容,别板着一张脸】
手势动作。【】
语速。
语调,语气。【声音要有起伏,有高低】
你对我们公司还要什么要了解的?【只用回答这些就行了 从3个里面挑2个】
咱们公司最近在做哪方面的项目?
咱们公司一般都用啥技术?
咱们公司一个项目团队有多少人?