·项目开发中遇到的问题
- 定时器分布式job问题
单机开发没问题,集群部署出现问题,同一时间多个实例的定时器执行超时待支付订单的删除,多个定时器线程都从数据库中redis中查询出需要进行解锁的同一订单,导致多个线程都去数据库进行解锁,很可能导致超卖问题出现,可以利用利用Redisson给定时任务上分布式锁,或使用xxjob
- rpc远程调用分页插件使用问题
在使用rpc远程调用时,不能在调用方使用分页插件来进行分页查询,因为分页插件的原理是利用动态代理+当前线程实现的。(分页插件原理。。。)在mybatis分页插件中,开启分页的方法会首先将分页查询的条件绑定到当前线程中,并为Executor核心对象生成代理对象,拦截query方法,在触发Executor的query方法时,会在触发代理对象的invoke方法,其中获取绑定到线程中的参数,实现分页查询,但当我们进行rpc时,服务调用方和提供方分别在不同的服务器上,所以也不能是同一线程。解决的方法就是当我们需要创建查询对象或者分页时,不要在调用方进行创建,而是在服务提供方进行创建,避免网络传输中出现的问题,或者也可以利用dubbo的隐式传参机制,使用RpcContext.getContext().setAttachment方法设置参数。。。(说一堆dubbo隐式传参)
- 一键登录短信发送同步问题
在登录接口中我们使用了mq调用了阿里的发送验证码接口,如果直接调用会造成同步请求问题,导致用户体验很差,用mq可以实现接口间的异步通信和解耦,所以使用了mq实现我们自己的接口和阿里接口的异步和解耦
- jpa的getOne和findById的区别
使用getOne时底层并不会立即去数据库中查询数据,而是会先返回一个查询对象的代理对象,当真正使用到这个代理对象时才会去数据库执行查询方法,并且查询不到会直接抛出异常,而findById在调用时会直接去数据库中进行查询,查询不到会返回null。在使用feign进行rpc调用中应该使用findById,如果使用了getOne会导致查询出的代理对象不能正常转换为json进行返回
- rpc远程调用传参问题
服务调用方在向服务提供方传递参数时会有些问题,比如当我们利用querywrapper在调用方构建查询条件并传递给提供方时,由于querywrapper不支持序列化的原因,会导致这个查询对象在网络传输过程中被丢失,使得查询结果出现问题;
-
Springmvc为什么可以在controller类的成员变量位置注入request域对象
-
用mq解决缓存双写不一致:当利用 redis为用户端做缓存时,在进行数据的更新就可能会导致redis中缓存的数据不能被及时更新,出现缓存双写不一致问题,针对这个问题我们使用了延迟双删的方法来解决,在更新数据之前先去redis中删除一次消息,再利用消息队列在更新数据后发送一条消息,再设置一个监听器监听消息队列中的消息,在监听器中进行消费,去redisi中二次删除对应更新的数据的缓存,第二次删除的原因主要是防止在高并发情况下,可能会有其他用户请求在第一次删除缓存之后、更新数据库之前又去数据库中查询,并把这个查询出的脏数据又写入redis中
-
feign远程调用需要进行登录认证的接口时,需要设置feign的远程调用拦截器,拦截器会在每次发起远程调用前拦截调用请求,在拦截器中利用getRequestAttributes方法先获取用户当前携带的token,将这个token利用requestTemplate设置到远程调用请求中,再发起远程调用请求,这样才能在服务提供方通过登录验证
-
feign远程调用中,远程调用接口返回值有泛型要指明泛型类型,同时服务提供方也需要在返回值指明泛型类型,否则返回的结果无法转换为json进行传输
-
nacos2.0底层服务提供方在注册时使用的是grpc框架,其中的grpc注册端口=web端口+1000,在docker上进行端口映射时,要同时映射这个端口和web端口才能正常注册服务的提供方和调用方,而之前是直接使用的http协议进行的注册
-
网关鉴权后怎么将获取用户的信息转给具体微服务:通过请求头携带信息,网关将解析的jwr令牌的的信息或redis缓存中读取到的信息放入请求头,再将请求交给具体微服务,每个微服务编写拦截器从请求体中获取信息并放入当前线程
-
对docker容器进行集群部署时,所有注册到nacos中的服务都是默认使用docker的内网ip作为注册地址,导致集群部署时nacos监听到的服务提供方只有一个,在创建并允许docker容器的命令中使用–network=host,设置后docker容器会使用宿主机的ip到nacos进行注册
-
jwt长度过长:将很长的信息放入redis
·正向代理,反向代理,负载均衡
正向代理是指客户端的行为,客户端直接发送请求到代理服务器,代理服务器再向实际想要访问的服务器发送请求。
反向代理又叫做转发,是服务器端的行为,客户端的动态资源请求先发送到集群服务器,集群服务器再通过一定的负载均衡算法将请求转发给后端的目标服务器。
通过集群的方式实现反向代理能够实现服务器的高并发和高可用性。
常用的负载均衡算法包括
<!--
轮询
根据客户端请求的时间先后顺序来访问不同的后端服务器,这也是使用nginx进行集群时的默认算法,当其中某台服务器挂掉后也能够正常访问其他服务器
-->
upstream backserver {
server 192.168.0.14;
server 192.168.0.15;
}
<!--
权重
为不同的后端服务器分配不同的权重,当访问时会按照权重比例进行随机访问,适合后端服务器性能不一致的情况
-->
upstream backserver {
server 192.168.0.14 weight=10;
server 192.168.0.15 weight=10;
}
<!--
ip_hash
根据请求的ip地址进行hash运算,根据运算结果确定访问的服务器,使同一ip每次都能访问同一服务器,解决了session的问题
-->
upstream backserver {
ip_hash;
server 192.168.0.14:88;
server 192.168.0.15:80;
}
<!--
fair
根据不同服务器的响应时间来进行分配
-->
upstream backserver {
server server1;
server server2;
fair;
}
<!--
url_hash
根据请求的url地址进行hash运算,根据不同的结果确定访问的服务器,保证用户访问同一url地址时请求同一服务器,使缓存效率较高
-->
upstream backserver {
server squid1:3128;
server squid2:3128;
hash $request_uri;
hash_method crc32;
}
nginx应用
- 静态资源服务器
- 反向代理,做服务器的集群
·分布式session问题
当配置了集群服务器后,后端会存在多台服务器,当用户多次进行访问时,根据不同的负载均衡算法可能会访问到不同的服务器,这些服务器由于是相互独立的,所以session不能进行共享,就出现了分布式session问题
解决方案
-
配置ip绑定的负载均衡算法,当同一ip地址进行请求时,会根据ip地址的hash运算结果判断请求哪一台具体的后端服务器,所以同一客户端在多次访问时能够访问同一服务器,但是当这台服务器挂掉后这部分客户端ip就会访问失败
-
配置服务器之间的session共享,但实际实现过程也会有问题,首先是只有相同的服务器内核可以设置session的共享,但实际我们的多台后端服务器可能是使用的不同内核,另外实现后也会造成数据的冗余,因为同一用户的session在每台服务器中都存在,造成了空间资源的浪费
-
jwt方案 (什么是jwt)
思想是服务器端不再负责保存session,服务器端只负责token令牌的生成和解析,而由客户端负责保存token令牌,通过jwt也能更好解决认证问题(在用户访问接口时先判断用户是否有对应的访问权限)
(jwt优点)
相比与传统session,第一个优点是支持跨域访问,第二个优点是能够实现无状态的登录,而session的实现需要通过cookie中保存的seesionId,第三个优点是能够更好适配移动端,比如ios和安卓,而这些移动平台不支持原生的cookie和session
(jwt组成)
jwt中的核心部分就是token令牌,token令牌由三部分组成,第一部分是头部,其中是token的基本信息,包括第三部分签名的加密方式,第二部分是载荷,其中存放一些非敏感的业务信息,比如用户名,但不能存放密码等信息,因为载荷部分采用的base64编码是可以解码的,第三部分由三个小部分组成,分别是头部base64后的编码,载荷base64后的编码和存放在服务器端的密钥,这三部分组合后再使用头部的不可解密的加密算法进行加密就是第三部分的签名
(jwt实现登录认证,即登录认证拦截器中的token验签过程)
在用户访问登录接口登录时,会先判断用户名和密码是否正确,正确的话就会为其签发token,我们可以使用多种方式进行token签发,比如用hutool时,我们只需要将我们后续可能用到的非敏感信息放在一个map集合中,将这个map集合和服务器端密钥一起传入createToken方法中,就能返回一个token字符串交给客户端进行保存。后续客户端每次请求时都会携带对应的token信息,在服务器端的拦截器中我们会进行验签,我们会将头部和载荷部分取出,加上服务器端存放的密钥,再使用头部声明的加密算法进行加密,看结果是否和客户端请求时的token第三部分签名是否一致,如果一致说明是服务器端所签发的token,可以放行,否则进行拦截。放行后我们还能从载荷中取出我们存放的信息,并放入当前线程进行保存(当使用jwt+uri+拦截器实现权限鉴定时,在登录认证拦截器中取出载荷中的uri绑定到线程中,然后在权限鉴定拦截器中对比用户当前访问的接口uri和当前线程中绑定的自己有的uri),后续业务中我们就能从线程中取出对应的信息,实现隐式传参。
jwt令牌续期问题
- 每次请求重新生成全新jwt令牌,在服务器端的登录认证拦截器中为用户的jwt进行续期,用户请求被登录认证拦截器拦截后,先对用户的token进行验证,(验证过程。。。)验证通过后将token进行解析,取出其中载荷部分的数据,并存入一个用户实体类对象中,之后为用户生成新的token,我们会在用户实体类中新增一个属性专门用于保存本次请求所生成的新token,这个属性不会在数据库中设置,然后把新token也存放到用户实体类对象中,之后我们就将这个对象绑定到当前线程,后续业务流程就能直接从当前线程直接取出用户信息,当业务方法执行完毕后我们会在统一的返回结果集方法中从当前线程中取出之前所新生成的token,并为这个token单独设计一个字段返回给前端。这样做就需要前端与我们配合,因为每次请求都会返回新token,前端就应该在架构中从我们返回的响应中取出这个token并存入cookie或者localstorage中进行保存
- 服务器用redis标记令牌失效时间,这里我们不会把整个token存入redis中,因为jwt的token令牌会很长,占用很多redis空间,如果直接存token的话还不如直接把用户信息存入redis中,直接使用redis实现登录认证,所以这里我们只会在redis中存入用户的唯一标识信息,用户在请求接口时,在登录认证
(redis实现登录认证,即登录认证拦截器中去redis中查询token是否没过期)
4. 使用redis缓存用户信息,适用于微服务项目中,用户在登录时先访问服务器端的登录服务器,登陆服务器根据登录信息到数据库中查询数据,为用户生成一个token令牌,将这个token令牌作为key,把查询到的用户信息存放到redis中,这里我们会设置redis的失效时间为2小时,然后将这个token令牌返回给客户端,客户端在之后使用其他登录业务时,就会携带token令牌,服务器端根据这个令牌到redis中查询之前所存储的登录信息,同时为了实现为用户token的续期,在每次到redis查询数据时都会将token失效时间重置为2小时,就是把token从redis中删除后重新添加,若查询不到则说明登录信息已经失效,用户需要重新登录若查询到,则将查询到的用户数据绑定到当前线程中。
·权限鉴定
以jwt进行权限鉴定时:
用户在登录时首先验证用户的登录信息,验证通过后根据用户id或用户名去数据库中查询用户所拥有的所有uri访问权限(或权限标记),并将这部分内容放入到token的载荷中,之和将生成的token返回给客户端,由客户端进行保存。客户端在每次请求时都会携带token,然后会被服务器端的登录认证拦截器拦截,拦截后首先进行验签,验证token是否合法,验证通过后对token进行解析,将token中的载荷解析出来,我们可以把这部分信息放入当前线程进行保存
以redis进行权限鉴定时:
用户登录时先验证用户登录信息,验证通过后去数据库中查询用户信息,将用户信息读取后,生成对应的token令牌,以token为key将用户信息存入redis中,并将token返回客户端,客户端之后每次请求都会携带token,之后被我们的登录认证拦截器拦截,
通过jwt+uri+权限鉴定拦截器实现
然后登录认证拦截器放行给权限鉴定拦截器,在权限鉴定拦截器中取出存放在当前线程中的权限uri信息,并通过request对象获取用户当前所访问的uri地址,将当前访问的uri地址与线程中保存的用户权限信息对比,如果包含所访问的权限uri,说明用户具有对应权限,进行放行,否则拦截
通过aop+自定义注解实现
之后放行用户访问对应的接口。在访问接口时就会调用我们所编写的权限aop通知方法,这个权限通知方法中会首先判断接口上方有没有被我们自定义的权限注解所标记,若没有则说明当前接口不需要进行权限鉴定,直接进行放行,若存在则说明需要进行鉴定。在鉴定时我们会从当前线程中取出用户所拥有的全部权限标记信息,并将其与用户访问的接口上的权限鉴定注解中的内容进行对比,若包含则说明用户拥有对应权限进行放行,否则进行拦截
怎么确保接口传输安全性?(接口间参数传递时不被窃取)
- 使用RSA2非对称加密,客户端配置公钥,请求时对token加密码。服务器端用户服务费配置私钥,对token解密,解密后的token才能从redis中查询缓存到的用户信息
- https协议
·springmvc访问流程
首先在web.xml中配置springmvc的前端控制器DispatcherServlet,设置需要哪些路径需要访问springmvc,并将springmvc.xml加载到其中,在springmvc中配置了springmvc的三大组件,其中处理器适配器和映射器一般都只需配置注解驱动,在处理器上配置RequestMapping注解即可,在用户请求对应页面时访问注解下面的处理器方法。在配置视图解析器时可以为视图加上特定的前缀和后缀,视图解析器的作用是解析ModelAndView对象中的参数,并返回View对象给前端控制器进行页面渲染。
在访问springmvc时,用户首先将请求传给前端控制器,前端控制器又将请求路径传给处理器映射器,处理器映射器会根据访问路径去匹配对应的controller层处理器方法和拦截器,之后处理器映射器会将匹配到的处理器和拦截器返回给前端控制器,之后前端控制器会将处理器和拦截器交给处理器适配器执行,我们会在处理器中编写业务代码,并将业务代码执行的结果数据绑定到一个model对象中,并将view层的地址作为字符串进行返回,处理器适配器会自动将model对象和view地址进行绑定,生成modleAndView对象,我们也可以直接返回ModelAndView对象给适配器。然后处理器适配器将ModelAndView对象返回给前端控制器,前端控制器再将ModelAndView对象传递给视图解析器,视图解析器中对ModelAndView对象进行解析,返回View对象给前端控制器,前端控制器根据View对象进行页面渲染并展示给用户
·转发和重定向的区别
转发只有一次请求,重定向会有两次请求
转发后放在request域中的参数在另一个页面仍然存在,重定向因为是不同的请求所以参数会被清除
转发不会在地址栏中显示新页面地址,重定向会显示
重定向的原理是服务器向浏览器返回302响应头并将重定向的地址放在location中,浏览器再根据location地址进行第二次请求
·springaop
aop是一种面向切面编程思想,纵向重复代码横向抽取,springaop的底层用到了动态代理来实现aop,如果被代理对象实现了接口会使用jdk动态代理,如果没有实现接口就会使用cglib,利用继承实现动态代理。
在开发中aop可以使用在事务管理中和权限鉴定和多数据源连接中,可以通过编码和声明两种方法实现aop的事务管理。
编码式的实现过程:在spring中有一个接口PlatformTransactionManager就是用于事务管理的,其中定义了3个方法,getTransaction方法是获取事务并判断是否开启事务,commit方法提交事务,rollback方法回滚事务。在getTranscation方法中需要传入一个TransactionDefinition事务信息对象,其中就封装了事务的信息,包括事务的隔离级别,传播行为等。其中会根据传播行为判断当前是否开启新事务,比如当传播行为为required时,若当前已开启事务就会加入到已开启的事务中,若当前未开启事务则会新建一个事务;当传播行为时required-new时会将当前已开启的事务挂起,再去新开启一个事务。
第三方的持久层技术都针对PlatformTransactionManager接口进行了实现,比如在使用mybatis时,我们就配置对DataSourceTransactionManager进行ioc,在其中注入数据库连接池。之后就可以使用spring提供的TransactionTemplate进行事务管理,在TransactionTemplate中注入DataSourceTransactionManager。
在使用时,我们将需要进行事务控制的代码放在一个TransactionCallback对象的doTransaction中,将TransactionCallback对象传递给transactionTemplate的execute方法就能完成事务控制。
TransactionTemplate的execute方法使用了模板方法设计模式,其中先调用了开始事务的方法,然后调用我们在TransactionCallback对象的doTransaction方法定义的业务代码,最后再提交事务。
声明式进行事务控制有两种配置方法,可以通过appliactionContext.xml和注解进行配置。
使用xml配置时,要分别配置通知和织入,配置通知时要先配置事务管理器transactionManager,然后配置各种方法的隔离级别,传播行为等属性,然后再配置织入,配置织入时先配置切入点表达式,spring会根据切入点表达式对对应的方法进行拦截,然后配置切面,即把切入点和通知相结合
使用注解进行配置时,只需要在xml中配置事务管理器transactionManager和开启aop事务的标签,然后在需要进行事务控制的service实现类上使用@Transactional注解,就会根据数据库配置的隔离级别和传播行为配置事务
·spring父子容器
- 子容器可以注入父容器管理的bean,如service,mapper
- 父容器不能注入子容器管理的bean
spring的父子容器是存在于spring和springmvc的原生环境中的,spring的父子容器其实就是map集合,父容器是保存在ServletContext中的map集合,子容器是保存在DispatcherServlet中的map集合,父容器会先于子容器进行创建,并且子容器中会保存父容器的引用。
父容器的创建是在web.xml中配置ContextLoadListener监听器,监听ServletConetxt域对象的创建,在创建后就创建父容器,创建时先从ServletContext域对象中读取全局配置的父容器配置文件,然后进行ioc和di,最后把父容器放入ServletContext域对象中保存
(父容器创建更详细版本:首先spring会通过我们在web.xml中配置的ContextLoadListener监听器监听创建servletContext对象的创建,servletContext对象是整个web项目共享的一个域对象,这个对象就相当于是项目本身,在这个监听器当中就会创建BeanFactory对象,BeanFactory是spring容器的顶级接口,spring容器在初始化过程中一般会使用ApplicationConetxt这个子接口,这个接口相比于BeanFactory接口,会定义更多的属性和方法,这里就会利用这个接口来创建spring容器,之后会读取我们配置的spring容器的配置文件,在配置文件中我们会定义很多bean标签,这些bean标签就相当于之后需要交给spring容器管理的对象,spring在读取到这些bean标签后,会将bean标签转化为BeanDefinition对象,这个对象会保存bean标签中配置的详细信息,之后就会刷新整个spring容器并且利用所有BeanDefinition对象初始化所有单例bean对象,然后再把这些bean对象放入到spring容器中,这一步就相当于完成了spring的ioc和di,这时spring父容器的初始化就已经完成。)
子容器的创建是在DispatcherServlet前端控制器创建时创建的,前端控制器实际就是一个servlet,默认是在第一次访问时创建,也可以配置项目启动时创建,但无论哪种创建方式,都是在ServletContext域对象创建后才创建的,所以一定是在父容器创建后才会创建子容器。在创建时会读取我们配置的springmvc配置文件中的内容,然后进行ioc和di,同时子容器创建时会获取父容器的引用,原因是父容器一般会存放Service层对象和Mapper层对象,而子容器中存放的是Controller层对象,在使用springmvc的三层结构时,我们需要在controller层注入service层的对象,也就是需要在子容器中注入父容器管理的对象,所以需要在子容器中通过父容器的引用进行注入,这也就是为什么可以在controller注入service对象,而不能在service对象中注入controller对象
·Mybatis插件
Mybatis中开放了一个Interceptor接口,这个接口可以针对sqlsession的四大核心对象Executor,StatementHandler,ParameterHandler和ResultSetHandler中特定的方法进行拦截,并且拦截后调用我们所编写的自定义插件代码进行功能的增强。
实现Mybatis的自定义插件时,我们需要编写一个插件类实现Mybatis中的Interceptor接口,在类上面使用Singnature注解指定需要进行拦截的sqlsession核心对象类型以及拦截的方法名,并重写其中的intercept方法,在其中编写我们需要进行增强的插件代码,然后需要在配置文件中通过plugin标签配置这个自定义插件类,后续在运行时如果我们所指定的核心对象的对应方法被调用就会自动帮我们调用intercept方法进行增强。
实现的原理实际上mybatis在底层使用了jdk动态代理的方式,mybatis会通过配置文件获取到我们所编写所有的插件类,并在底层通过一个for循环对所有插件类进行迭代,循环为被代理对象Executor创建代理对象,最后for循环结束后生成的Executor对象其中就包含了所有的插件方法,当核心对象的方法被调用时,会首先判断当前被调用的方法是否有和它对应的插件方法,若存在的话就会调用对应的插件方法,否则就放行。
一般常用的插件是PageHelp分页插件,在使用时先在配置文件中配置开启分页插件,然后在需要开启分页的查询方法中调用PageHelper的startPage方法开启分页,传入当前页数和分页大小开启分页,在startPage方法中会将我们传入的参数绑定到当前线程中。开启分页插件后,就会为Executor对象生成对应的动态代理对象,其invoke方法中就会从当前线程中取出分页信息,并动态生成包含limt关键字的sql语句。然后当我们调用Executor对象的query方法进行查询时就会被分页插件的拦截器所拦截,并触发其中的invoke方法生成包含分页查询的sql语句,实现分页查询。
·Springmvc为什么可以在controller类的成员变量位置注入request域对象
问题:request域对象的生命周期应该只有每一次请求,客户端发出请求时才创建,请求结束后就被销毁。Springmvc中controller对象是保存在dispatcherServlet的map集合中的,也就是spring子容器中的,并且所有controller对象都是单例的,是在springmvc配置文件读取后就已经创建完成的,但其中的成员变量中却可以注入request这个域对象。并且可以正常调用其中的方法获取request域对象中的参数。相当于controller注入了一个不存在的对象。
实际上当客户端请求到达后端时,在springmvc的dispatcherServlet前端控制器中的service方法会将创建的request对象绑定到当前线程中,而在controller中注入的对象并不是客户端创建的request对象,而是根据根据jdk动态代理机制和ServletRequest接口所实现的代理对象,当我们调用这个代理对象的方法时,就会触发代理对象中的invoke方法,而在invoke方法中,就会从当前线程获取所绑定的request对象,并调用我们针对被代理对象调用的request方法进行执行。
·spring的ioc和di
ioc是指控制反转,核心思想就是将spring中的单例对象交给spring容器进行创建和管理,无需手动进行创建。在实现ioc时,要首先通过配置的方式告诉spring哪些类需要进行ioc,之和spring会在底层使用反射或者工厂设计模式为我们生成对应对象并放入spring容器中进行管理。
sping中ioc有多种创建对象的方式,包括构造方法,静态工厂,实例化工厂和实现FactoryBean接口。。。
同时我们也可以使用注解实现ioc,一般常用的注解有Component,Controller,Service和Respository
di是指依赖注入,就是在spring容器ioc完成后,对其中对象的属性进行初始化,实现di的方式包括构造器di,set方法di和集合di。。。
同时我们也可以使用注解进行di,注解实现di包括Autowired和Resources。。。
·spring的BeanFactory和FactoryBean的区别
BeanFactory是spring容器的顶级接口,其中定义了一些spring容器中的基本属性和方法,这个接口一般不会对外使用,只是spring源码内部会调用这个接口,我们一般会使用他的子接口ApplicationContext,在ApplicationContext中会定义更丰富的属性和方法,这个子接口又有多个不同的实现类,这些实现类可以以不同的方式来初始化spring容器,比如ClassPathXmlApplicationContext实现类可以通过我们传入的相对路径的位置找到srping容器的配置文件,根据配置文件来进行Spring容器的ioc和di
FactoryBean是spring中定义的工厂类接口,任何类只要实现了这个接口并重新其中的getObject方法,就会被spring识别为工厂类,spring在针对这个工厂类进行ioc时,会自动调用工厂类中的getObject方法,并将getObject方法中创建的对象放入spring容器中进行管理,FactoryBean的应用场景:
- 应用在一些第三方技术框架与spring进行整合的时候进行使用,因为第三方框架的实现细节spring和我们调用者可能都不清楚,所以如果第三方框架的开发者能够编写一个FactoryBean接口的实现类,在其中的getObject方法中创建并返回这个第三方框架中重要的类对象,我们作为调用者就能直接针对这个FactoryBean接口的实现类进行ioc,这样就能很方便的完成第三方框架与spring框架的整合
- 利用FactoryBean也可以生成动态代理对象,并将其交给spring管理,比如在Mybatis底层也是使用这个接口来实现mapper接口的动态代理对象的生成的。。。。在Feign的远程调用框架中的FeignClietn对象也是根据FactoryBean生成的。。。
·resultmap和resulttype的区别
resultMap和resultType都是在mapper接口对应的xml文件中用于指定查询结果的映射的,resultType是自动映射,我们只需要为其指定想要进行映射的实体类,查询后就能帮我们自动返回对应的实体结果。而resultMap可以进行手动映射,在其中配置result标签,标签的column属性指定数据库查询出的字段名,property属性指定映射到实体类时的属性名,resultMap还能实现多表查询时一对一或一对多的关系映射,进行一对一映射时通过配置association标签,一对多时配置collection标签,标签的column属性指定根据当前表中哪个字段去另一张表进行查询,property属性指定查询的结果映射到实体的属性名,select属性指定需要通过哪个mapper层的查询方法到另一张表进行查询
示例:
<!--type指定映射的实体类-->
<resultMap id="orderResultMap" type="com.gxa.pojo.Order">
<!--映射主键
column为数据库列名
property为pojo属性名
两者间的类型会自动进行转换-->
<id column="id" property="id"/>
<!--配置普通属性-->
<result column="order_number" property="orderNumber"/>
<result column="payment" property="payment"/>
<result column="payment_type" property="paymentType"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="user_id" property="userId"/>
<result column="buyer_message" property="buyerMessage"/>
<!--一对一映射关系时可以使用association标签将另一个表的数据映射到本类中对应的成员属性中-->
<!--property指定Order实体类中作为成员属性存放的另一个实体类对象名-->
<!--column指定根据当前表中哪一列作为查询条件-->
<!--select指定使用column进行查询时所使用的查询语句,这里可以直接使用其他mapper.xml中的查询语句,前面跟另一个mapper的命名空间-->
<!--查询出来的每条order表中的数据都会执行一次这个映射,拿到该条数据的user_id去执行findUserById,并将查询得到的对象设置到该条order表查询结果对象中的user成员上-->
<association property="user" column="user_id" select="com.gxa.mapper.UserMapper.findUserById"/>
</resultMap>
·RPC注册中心流程
首先配置服务提供方,在配置文件中配置注册中心的地址,提供方提供服务访问的端口及通过端口所暴露的服务接口,配置暴露的服务接口时要分别配置接口全限定名和接口实现类对象在spring容器中的对象名。
再配置服务调用方,同样也要配置注册中心的地址,还有在调用方的controller层生成代理对象的接口
当服务提供方的服务器启动时,根据配置文件中配置的注册中心地址到注册中心中注册节点,首先根据接口的全限定名生成一个持久化节点,然后再根据服务提供方所配置的服务调用ip地址和端口生成对应的临时节点。
当服务调用方启动时会根据配置的注册中心地址,在其中寻找所有的提供方所注册的临时节点调用地址,将其进行缓存,并针对注册中心中的持久化节点建立监听,随时鉴定其临时子节点的变化。我们会在controller层中注入服务提供方的service实现类对象,但实际上注入的是根据我们配置的与调用方完全一致的service接口所创建的动态代理对象,当我们调用其任何方法时,会触发代理对象的invoke方法,在invoke方法中会先获取当前的服务提供方临时节点列表,再根据我们配置的负载均衡算法决定调用哪个临时节点,如果我们使用的是dubbo框架进行rpc的话,就会通过nio的非阻塞方式发起socket客户端请求,nio的通信方式相比bio的传统通信方式,其服务端的线程利用率会提高很多,服务端只需要一个线程就能和多个客户端进行通信,请求建立后就会传递调用的方法与参数信息,传递参数时会使用hessian进行序列化,hessian的序列化相比java原生的序列化方式效率会提高很多,之后在对应的服务提供方的serverSocket中获取要调用的方法和参数并调用本地的实现类方法获取执行结果,最后将执行结果通过socket再响应给调用方完成调用
·NIO
BIO与NIO区别
- bio是同步阻塞,是面向流的,优点是编程简单易理解,缺点是可靠性低和吞吐量小,当服务端在收到请求后,处理完成请求前将一直被阻塞,导致线程利用率低,默认情况下服务器端需要对每个客户建立一个线程与之通讯。
- nio是同步非阻塞,是面向缓冲区的,优点是可靠性高和吞吐量大,但编程复杂难理解,原理是通过选择器主动轮询通道,查看请求是否完成来判断当前是否继续执行,提高线程利用率。
NIO三大核心
缓冲区:本质上是一块进行读写数据的内存空间,用于和通道进行交互,分为读模式和写模式,读模式的position是头部,limit是最后一个元素的位置,写模式中position是最后一个元素位置,limit是缓冲区最大下标,缓冲区又可以分为直接与非直接缓冲,直接是非堆内存缓冲,jvm在直接内存上具有更高性能,但它的申请需要更高性能。非直接是堆内存缓冲区,操作时会先从非直接内存复制到直接内存,再进行io操作,所以操作效率不高,但申请时需要的性能也不高。
通道:通道作用是将原缓冲区与目标缓冲区要交换的数据进行传输,与流相比,通道既可以读又可以写,是双向的,而流只能读或写,是单向的,在nio中主要使用serverSocketChannel和socketChannel,前者是监听客户端新建立tcp连接的通道,后者是用来通过tcp读写网络中数据的通道。
选择器:主要是用来检查多个通道的状态是否是处于可读可写的,只需要一个线程进行管理,执行时会通过轮询,循环判断每个通道的状态。
当服务器端使用nio方案进行响应时,首先创建一个选择器,并为其绑定一个accpet接收事件,然后使用一个while循环判断当前选择器是否有绑定的事件处于就绪状态,再对所有就绪事件进行迭代,先判断当前事件是否是accept接收事件,当是接收事件时说明有一个新客户端在发送连接请求,所以之后就获取客户端的通道socketChannel,并将其的io事件绑定到选择器上;之后再判断当前事件是否是io事件,若是io事件就从其通道中的缓冲区读取数据并清空缓冲区。
客户端在请求时先创建通道和缓冲区,再向缓冲区中写入数据,然后将缓冲区放入通道中,最后关闭通道,服务器端就能获取客户端再通道中的数据信息。
dubbo
dubbo是用于进行rpc的框架,在进行rpc时默认使用的通信协议是dubbo协议,这是一种基于nio和netty的非阻塞通信协议,能实现一个服务器端进程就能和多个客户端进行通信,默认的序列化协议是hessian。
dubbo工作流程
服务提供方配置注册中心地址、暴露的端口号、提供的服务的service接口和实现类,提供方启动时会去注册中心注册自己的service接口和实现类信息;
服务调用方配置注册中心地址、需要调用的服务的service接口,service接口要和提供方一致,启动后会去注册中心拉取提供方的列表信息,并且缓存到本地吗,这个缓存信息会根据不同的注册中心定时更新,保持与注册中心的信息一致,比如如果是zk这里就是建立的tcp长连接,如果是eureka会定时拉取注册中心信息,如果是nacos就同时会有以上两种方式,不过长连接是以udp方式连接的,之后会根据配置的service接口生成了动态代理对象,所以可以在服务调用方的controller层注入service接口,当调用方触发service动态代理对象的方法时,会根据被代理的service接口去本地缓存的提供方信息中寻找对应的提供方service实现类,然后将参数通过hessian进行序列化,以socket的方式发起rpc请求,底层走的netty所封装的nio发起的SocketChannel请求,提供方通过ServerSocketChannel监听到调用方的请求,然后执行service实现类中方法,然后通过socket响应处理结果给服务调用方
支持的通信协议
dubbo协议 网络(nio netty)
rmi 走的java二进制 多个短连接 适合消费者和提供者数量不多 适合于文件传输
hessian 多个短连接,适用于提供者的数量比消费者还要多,适用于文件传输
http协议 走json序列化
webservice 走soa文本序列化
支持的序列化协议
hessian(默认) java的序列化 json soa
负载均衡算法
轮询、随机、最短时间响应
dubbo协议特点
是基于tcp的单连接和长连接,通信方式是nio的非阻塞,数据传输使用的是hessian序列化协议,适用于数据量小但并发高、服务消费者远大于提供者的情况,不适用传输大数据量的文件如视频等
dubbo安全性如何得到保障
在有注册中心的情况下,可以通过dubbo 提供方中的路由规则,来指定固定ip的消费方来访问
·阻塞io与非阻塞io区别(bio和nio)
在阻塞io中,当客户端与服务器端一个线程建立通信后,直到客户端断开连接前,服务器这个线程就只能处理这一个客户端的请求,当客户端的请求被阻塞后(比如因为用户没有输入而阻塞),服务器对应的线程由于没有收到客户端对应请求也会被阻塞,导致服务器线程利用率很低。
而非阻塞io中,客户端会通过通道与服务器的选择器进行连接,选择器只需要一个线程进行执行,同时一个选择器上可以注册多个通道事件,所以一个服务器线程可以处理多个客户端的请求,选择器会不断轮询它所管理的所有通道事件,当有客户端发生对应的事件时,选择器就会执行对应的方法进行响应。所以单个客户端请求被阻塞并不影响服务器的选择器执行其他通道的事件,实现线程的高利用率
·同步和异步的区别
同步方法是指当它被调用后,调用方必须要等待同步方法执行完毕才能继续执行后续的代码;而异步方法在被调用后会立即给调用方返回一个结果,使调用方可以马上继续执行后续代码,但这个结果并不是真实的调用结果,真实的调用结果会在异步方法执行完毕后通过消息或者回调再回头通知调用方。
典型的异步:
- callable接口实现多线程时,子线程的结果通过Futrue对象返回
- 一键登录发送短信验证码使用kafka
·redis、缓存、rpc相关
memeched、redis、mongodb对比
memeched:没有备份机制,所有数据都存放在内存中,只有string一种格式的数据
redis:性能比memeched低一点,但支持备份,数据会保存在硬盘中,其中所有操作都是原子性的,采用单线程处理所有业务,所以命令是一条一条执行的,不用考虑并发的情况
mongodb:只支持单表,没有事务,一般存放一些不太重要的数据
redis中数据类型
string:最常用的类型,可用于做缓存,分布式或者分布式锁
hash:Hashmap格式,其中元素也是以key和value格式存放,可以用于做商城系统中的购物车,在redis的key中存放用户id,在hash的field中存放每个购物车中商品的id,value中存放商品具体信息
列表(链表):支持重复元素,朋友圈点赞,可以使用列表的push方法添加点赞信息
无序集合set、有序集合set:不允许重复元素
stream:用于实现消息队列
redis中的列表数据类型如何实现栈和队列?
在列表一头同时使用push和pop就是栈;一头push另一头pop就是队列
缓存雪崩是什么?怎么解决?
缓存雪崩是指当给大量缓存数据设置同一缓存有效期时,在有效期到达后就会同时失效,这时间就会导致数据库访问压力突然增大,出现缓存雪崩。
在解决时,我们可以在设置缓存时给在原有缓存时间基础上再加上一个随机时间,这样缓存的到期时间会在一个范围内,避免缓存雪崩
缓存穿透是什么?怎么解决?
缓存穿透是指黑客可能会抓住我们数据库中不存在的数据,进行不停重复查询,由于我们在使用redis做缓存时,在查询时会首先在redis中查询是否有缓存,没有再去数据库查询,黑客进行攻击时反复查询的是一条不存在的数据,就会导致数据库压力增大。
在解决时,可以在redis中存放标记信息或者设置布隆拦截器
方法一:redis存放标记信息,是指当用户访问数据库时若第一次没有在数据库中查询到对应的数据,就将查询这个数据时的查询条件写入redis缓存中,在每次去数据库查询前就先看redis缓存中是否存在这个查询条件,若存在就直接返回空,这样做的话还需要改造一下新增数据的接口,在新增数据前也要去redis中查询一下是否有这个数据的查询条件,若存在就将其从redis中删除。
方法二:使用布隆拦截器,我们可以在服务器端直接将所有已存在的数据的查询条件存储在集合中,再设置一个拦截器拦截所有查询请求,先看查询请求的查询条件是否存在于这个集合中,若不存在直接拦截
nginx限流算法有哪些?
限流算法是用于限制服务器接收并处理请求的流量,避免同一时间大量请求访问导致宕机。
算法一:令牌桶
首先创建一个令牌桶用于存放令牌,令牌以固定的速率每秒生成并放入桶中,若当前令牌总数超过桶容量则直接丢弃,然后设置一个拦截器拦截所有请求,在拦截器中为每个请求分配一个令牌,只有有令牌的请求才进行放行,进行对应接口的访问,若令牌桶中令牌被消耗完则将没有分配到令牌的请求缓存起来,直到为其分配新产生的令牌。
算法二:漏斗算法
相比于令牌桶来说,漏斗算法只有一个用于缓存请求的队列,请求在到达服务器后会放入这个队列,队列中的所有请求以固定的速度交给后端接口进行处理,当请求量大于队列长度时就直接丢弃这部分请求
缓存预热是什么?怎么实现?
缓存预热是指当项目刚启动时,redis中还没有缓存数据,这时若突然有大量请求来进行查询,就会造成数据库压力增大,所以我们就需要在项目启动时就进行数据的缓存。
实现方法:编写缓存预热的类,实现ApplicationListener接口,这个接口可以监听到spring容器的创建,在创建完成后就会自动调用其中的onAppliactionEvent方法,所以我们只需要在其中编写查询数据库并将数据放入redis缓存的代码
缓存击穿是什么?怎么解决?
缓存击穿是指服务器中的某一热点数据突然失效,导致所有查询这一热点数据的请求都去访问数据库,造成数据库压力增大
解决方案就是设置这部分热点数据永不过期
服务雪崩(集群容错机制)是什么?怎么解决?
服务雪崩是指在处理客户端请求过程中,一个服务器可能会嵌套调用多个不同的服务器中的方法,若这个过程中任何一个服务器出现宕机现象,就会导致调用这个服务器方法的服务器出现大量请求的堆积,最后导致这个上层服务器也被宕机,出现连锁反应,使整个后端完全瘫痪
解决方法就是设置超时机制,当服务消费者在调用服务生产者的方法时,设置一个最大响应时间,若超时未响应结果直接抛出超时异常。当消费者发现响应超时后,会调用服务器的集群容错机制,集群容错机制包括失败重试和快速重试,失败重试是指在响应超时后会尝试去调用集群中的其他服务生产者,这也是dubbo的默认机制,默认会进行两次失败重试,我们可以通过配置retries来指定重试次数;快速失败就是指超时后不进行重试
dubbo幂等性问题是什么?怎么解决?
是指在进行rpc远程调用时,由于网络延迟原因,服务生产者响应服务调用者的时间超过了服务消费者所设置的最大响应时间,导致服务消费者误以为调用者没有收到调用请求,所以又进行多次重试调用,导致同一调用请求被重复执行多次。
方法一:直接设置消费者不进行超时重试,retries=0
方法二:一般受这种重复消费问题影响最大的就是插入操作,所以我们可以针对每条数据生成一个唯一标识,在进行插入操作前先在数据库中查看是否已经存在该标识的数据,如果存在就不在插入;或者我们也可以将这个唯一标识存放在redis中,在方法被执行完成后就将标识存入redis中,之后每次执行方法前都现在redis中查询是否有对应的标记
dubbo的spi
dubbo的spi类似于mybatis的插件,我们可以通过spi来对dubbo中的一些基本方法进行自定义更改,比如我们可以自定义dubbo的通信协议、序列化协议和负载均衡算法等等
dubbo的隐式传参
实现时,在服务调用方使用
RpcContext.getContext().setAttachment(“name”,“刘三石”);
服务提供方使用
String name = RpcContext.getContext().getAttachment(“name”);
就能取出调用方所传递的参数信息
原理:在调用方使用setAttachment绑定参数时,会将这个参数绑定到调用方的当前线程中,在进行rpc远程调用前会被调用方的拦截器拦截,拦截后将线程中绑定的参数信息设置到一个invocation对象中,并将这个invocation对象传递给服务提供方,而提供方也会对应有一个过滤器,在过滤器中会取出invocation对象中的参数,并将参数绑定到服务提供方的线程中,所以我们就能通过getAttachment得到参数信息
消息队列mq(为什么要使用mq)
(mq可以解决的问题)mq是用于存放消息的队列,通过mq
-
可以实现多个服务器之间的异步处理(即mq解决缓存双写不一致问题),(以多级缓存中缓存不一致来举例),又比如在发送短信验证码时也可以使用mq实现异步调用和解耦;
-
可以实现两个系统之间的接口解耦,比如针对上面的这个例子,如果我们直接在管理端通过远程调用来调用患者端操作redis的接口,就会增加两个系统的耦合性,如果后期针对患者端redis接口进行了修改,这时候也要修改管理端的调用接口,实现耦合性增大,这里我们就可以用mq实现消息队列,实现系统间解耦
-
可以实现流量削峰,比如系统中某些接口可能会被用户大量频繁访问,这些访问都可能会对数据库进行操作,造成数据库压力不断增大,所以在请求和数据库操作之间可以使用mq来进行消息传递
mq的jms中点对点模式
消息生产者向mq发送消息,不论消费者是否正常运行,都不影响生产者向mq发送消息,消费者和生产者没有依赖性,这个消息只能有一个对应的消息消费者,消费者在消费时会从mq中取出对应的消息,在成功执行完消费逻辑代码后再响应mq,mq就会将这个消息从mq中删除
mq的jms中订阅模式
同一主题消息可以有多个订阅者进行消费,订阅模式下发布者和订阅者有依赖性
mq的实现
在消息生产者一方生成消息,将其发送到mq中,在消息消费方设置一个监听器,监听mq中对应通道的消息,当发现有消息时就将其读取,并执行后续的消费者业务代码
mq中的重复消费是什么(消费者幂等性问题)?怎么解决?
在mq当中,我们一般会使用手动签收消息,只有消息被消费者手动签收后,消息才会从mq中移除,但在实际项目中,可能由于网络原因,导致消费方法被执行后,消息被签收时的时间已经经过了mq的最大超时响应时间,又或者由于程序的原因,导致消费方法被执行后,到消息被签收前程序发生异常,导致消息不能被正常签收。如果消息被消费了但没有被正常签收,mq就会重复发送,导致重复消费,出现幂等性问题。
解决方法一:通过redis进行唯一标记,消息生产者在发送消息时携带一个唯一标记,消费者在执行完业务代码后就马上将这个标记写入redis中,之后每次执行消费逻辑代码前都判断这个标记是否在redis中已存在
解决方法二:为每条数据生成唯一标记,当消费逻辑代码中进行插入时,先判断当前这个标记是否在数据库中已存在
解决方法三:增加kafka消息队列的最大超时等待时间,让消费者消费后kafka的消息能够被签收,避免因为网络波动原因产生的重复消费
springboot
springboot自动装配原理
springboot项目在启动时会自动加载所有jar包,并会检测这些jar包中是否存在meta-inf/spring.factories文件,如果有就会读取其中内容,其中会配置很多自动装配类的代码,之后就会加载这些自动装配类。其中定义的每个自动装配类在定义时都会使用一些特定的注解,比如有@Configuration注解,用于标记当前类属于java配置类,其中就能编写springboot的java配置代码,一般还有@ConditionalOnClass注解,这个注解可以检测在当前项目中是否存在注解参数中标记的类,若存在才会执行这个自动装配类中的代码,否则就不会执行,所以当我们导入的对应的starter依赖后,就能自动帮我们调用自动装配类进行配置
springboot项目启动时会首先扫描所有项目中的jar包,如果在jar包中发现存在META-INF/spring.factories文件,则说明当前是一个自动装配的jar包,之后会读取这个自动装配jar包中的properties类,properties类中会定义在装配时可能会用到的自定义参数,这个properties类其实就相当于properties配置文件,然后再读取当前项目中的application.yml文件,我们可能会在yml文件中针对自动装配的参数进行配置,springboot就会将properties类中的属性与yml文件中我们配置的自动装配参数根据名字一一匹配,将参数的值设置到properties类对象的属性中,然后就会执行在factories文件中的装配方法,这些装配方法在会根据方法上标记的注解按照一定的规则进行装配,比如常见的自动装配注解有:
-
AutoConfiguration:标记当前装配类是java配置类,java配置类其实就相当于使用xml方式配置时的xml文件,执行其中的装配方法就是在进行ioc和di,这个注解其实就是对Configuration注解的包装,相比Configuration,AutoConfiguration可以控制当前装配类中装配方法的具体执行时间,我们可以指定当前装配类在某个类装配之后或者之前进行装配。
-
ConditionalOnClass:springboot可以通过这个注解来决定是否执行装配类中的代码,当springboot检测到当前运行环境中存在注解中参数的字节码对象后才会进行装配,否则不会进行装配
-
ConfigurationProperties:用于标记当前类属于Properties配置类,用于代替properties配置文件,和yml文件配合完成自动装配参数的注入,这个注解一般是我们在自己编写springboot的自动装配类时才会使用
-
EnableConfigurationProperties:用于引入Properties配置类,在springboot将yml中的参数注入到Properties类的属性中后,通过这个注解来引入对应的配置类,在自动装配类的成员属性位置使用Autowired注解就能获取到对应的Properties配置类对象,这样就能在进行装配时注入其中的参数信息
一个自动装配类中可以定义多个不同的自动装配方法,这些自动装配方法都会用Bean注解标记,执行后返回一个装配完成的被装配类对象给spring容器,所以每个自动装配方法在执行时就相当于在进行ioc,并且这些自动装配方法之间应该时相互关联的,即一个装配方法返回的对象会在其他装配方法中作为参数进行传递,这个参数传递的过程其实就相当于在进行di,在整个自动装配类中所有自动装配方法执行完毕后,就相当于整个xml配置文件加载完毕,完成整个自动装配过程
springboot的常用注解
- ConfigurationProperties:用于标记当前类属于Properties配置类,会配合yml文件在调用时生成对应的配置参数
- SpringBootConfiguration、AutoConfiguration:对于Configuration的包装,用于标记当前类属于java配置类,代替xml文件进行配置,前者一般标记在自动装配类上,可以指定当前自动装配类的具体装配时间
- ConditionalOnClass:标记在自动装配类上,表示只有当当前运行环境中存在参数中所指明的类才会进行自动装配
- EnableConfigurationProperties:标记在自动装配类上,可以取得其中参数的Properties类中配置的参数信息
springboot项目打包成jar和war的区别
- springboot项目的jar包中的tomcat容器是生命周期是编译级别,即在打包后也会存在,所以在使用时可以直接启动,无需再配置外部的web容器;而war包中的tomcat容器的生命周期是保护级别,即只在开发时才能使用tomcat,打包后的包内没有tomcat容器,需要自己配置外部web容器才能运行
- springboot项目的jar包在启动时直接运行入口类即可,而war包会多生成一个ServletInitializer类,这个类中会有个configure方法,这个方法会调用springboot的入口方法,在外部容器运行时,就会自动运行这个configure方法,通过这个方法来启动springboot项目
springboot项目打包成的jar和普通jar的区别
springboot的jar包中包含tomcat容器,所以能够直接通过命令运行,是一个完整的独立项目,而普通jar包一般只是一个组件,是被其他项目所引入的依赖,而且也不能直接运行
自定义自动装配类
以自定义redis工具类的自动装配为例
-
先创建maven项目,在maven基础上编写自动装配项目。
-
在pom中导入自动装配必须的一些组件以及我们想要进行装配的类的jar包,比如这里我们就要导入redis的jar包
-
编写被装配类的具体方法,比如这里我们就是需要编写针对redis进行操作的代码,我们可以将对redis的增删改查的底层代码进行封装
-
编写Properties类,Properties类是用于代替properties配置文件的配置类,我们可以在其中定义一些使用者在使用时可能需要自定义的参数信息,将他们作为成员属性定义在类中,比如这里我们就需要定义redis连接池的连接地址,访问的端口号等信息
-
依次编写自动装配类,自动装配类其实就是用于代替xml配置文件的,我们可以使用多个不同注解来对装配过程进行控制(扯到自动装配注解上。。。),在装配时我们一般都需要进行多次装配才能完成整个自动装配过程,比如在这里我们需要先装配redis连接池配置对象,再利用连接池配置对象装配连接池对象。最后再利用连接池对象装配redis工具类对象,每个对象在完成装配后就相当于完成了ioc和di,所有装配方法执行完成后,就会将redis工具类对象放入spring容器中
-
编写自动装配类的spring.factories文件,这个文件是所有springboot的自动装配类都必须的,springboot项目在启动时会首先扫描项目中所有jar包,并看每个jar包中是否包含resources/META-INF/spring.factories文件,若存在则说明当前类属于自动装配类,其中需要指明自动装配项目中的所有自动装配类,springboot会一一执行其中的所有装配方法完成装配
-
将整个项目打包为jar包,其他项目引入,配置yml中的参数,就能正常使用了
登录问题(一键登录、重复登录、app端长时间登录、微信授权登录)
如何实现一键登录(手机验证码登录)
实现登录时,从整体架构上来说,我们会先去数据库查询用户信息,然后生成一个随机token令牌,以token令牌为key将用户信息写入到redis缓存中,并且这个token令牌会返回给客户端自行保存,这个对应的缓存信息我们设置的失效时间是最后一次操作的2小时后。用户在登录后访问需要进行登录的接口时,用户会携带这个token到redis中查看是否存在对应的数据,若没找到说明信息已经失效,所以我们是通过redis中是否存在用户端所存放的token来判断当前登录状态是否有效的。为了解决用户在同一种终端可能进行重复登录,导致redis中出现同一用户的多条数据,我们在数据库表中设计了几个字段,专门用于存放用户最后一次登录时为其生成存放在redis中的token令牌。为了实现用户在app端的长时间登录,我们又在表中设计了一个刷新token,可以根据这个刷新token直接在数据库查询到用户的具体信息。
具体来说,用户在客户端输入手机号点击发送短信请求后端的短信接口后,我们会先为用户生成一个验证码,然后将验证码写入redis进行后续验证,之后我们使用了kafka的mq,我们将验证码和手机号信息发送给kafka,同时项目中会有一个监听器监听kafka中的短信验证消息,当发现有消息就会在监听器中解析消息,并调用阿里的发送验证码接口为用户发送验证码,这里之所以会使用到kafka是为了实现接口间的异步通信以及解耦,因为如果我们直接在自己的接口中调用阿里发送短信的接口,就相当于在使用同步的通信方式,如果阿里的服务器出现故障,出现网络延迟,就会导致客户端要等很久才能收到消息,而在收到消息之前整个登录页面就会被完全阻塞,这样就会影响用户体验,而使用kafka用消息通知阿里接口执行时,就能实现两个接口的异步通信,用户点击获取验证码按钮后就能直接返回发送成功的信息;另外由于这个阿里发送短信的接口和我们的接口并不属于同一个项目,直接调用会让两个接口的耦合性增大,不利于后期维护,这也是使用kafka的一个原因。
当用户再输入验证码点击登录时,进入一键登录接口,在接口中先在redis中根据用户手机号查询用户输入的验证码是否和redis存的验证码一致,如果一致再去数据库中根据用户的手机号查询该用户是否已经注册,若已注册直接获取用户对应的信息,由于已注册用户可能在登陆时会出现重复登录的问题,在这里我们会先将从数据库中查询到的用户最后一次登陆时生成的token从redis中删除,若不存在则匿名为用户注册一个新账号,然后为用户的本次登录生成一个新的随机的token,然后将查询到的用户数据以这个新的token为key写入redis中,之后我们会判断当前用户是否是在app端进行的登录,如果是的话就要为用户实现长时间登录,所以这里我们就会再为用户生成一个随机的刷新token,我们设置这个刷新token的失效时间为3个月,并且将失效时间也记录在数据库表中,最后将这些信息保存到数据库中,然后返回redis中存放的token给客户端,如果这里用户是用app登陆的,我们就再将刷新token也返回给客户端保存。针对于app用户来说,若用户存放在redis中的信息失效了,我们就直接利用app端保存的刷新token再去数据库查询,查询时再生成新token和新刷新token,将查询到的数据写入redis缓存中,再将这两个token返回给app端保存,这样就能实现长时间登录。
对于微信授权登录的实现,首先是前端需要自己去请求wx.login接口,获取一个临时的登录验证凭证,然后前端将这个登录凭证传递给后端wx登录接口,在后端我们会利用这个code临时凭证+appid+secret再去请求微信的登录授权接口,验证通过后会微信会返回给我们一个openid和session_key,openid是每个微信用户的唯一标识,在这里我们会先利用这个唯一标识先去数据库表中查询是否存在对应的用户,如果存在则直接返回对应的用户信息,并且和之一样,先去redis中删除这个token的缓存以防止重复登录的问题,然后生成随机token,并将这次生成的token存入数据库表中对应记录最后一次token的字段,然后将token、openid和session_key一起返回给前端。这就是微信授权登录流程
nginx配置网关
通过配置反向代理来实现,先在nginx中配置统一的访问地址和端口号,然后根据访问路径的不同,设置多个不同的location,这些不同的location就指向不同的服务器项目地址
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8088;
server_name 127.0.0.1;
location /duker-api {
proxy_pass http://127.0.0.1:9026/$request_uri; # $scheme://$host$request_uri;
index index.html index.htm;
}
location /duker-server-api {
proxy_pass http://127.0.0.1:9528/$request_uri; # $scheme://$host$request_uri;
index index.html index.htm;
}
}
}
说明:两个api访问路径:/duker-api、/duker-server-api,当访问/duker-api时,让其访问到http://127.0.0.1:9026/duker-api/xx 服务;当访问/duker-server-api时,让其fang访问到http://127.0.0.1:9528/duker-server-api/xx 服务。
kafka
主题、分区、副本、偏移量
主题:是kafka在逻辑上对消息的分类,同一种业务操作种的消息可以被归为一类主题
分区:当配置kafka的集群时,同一主题会被分为多个分区,这些分区会分别存放在不同的broker上,在生成该主题的消息时,消息会被发送到其中某个broker中对应的分区中,分区可以解决存储容量的问题
副本:指同一分区会在多个不同的broker中存放,副本的存在可以实现高可用性
偏移量:记录每个分区中下一条将要发送给消费者的消息的序号,kafka默认将偏移量存放到zk中
kafka是如何实现高可用的
分区+副本
auto-offset-reset参数的区别
该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理,当offset有效时,都是从offset处开始读取,当offset无效时,earliest从头开始消费,latest从最新的数据开始消费
如何实现单播、多播、广播?
首先kafka中有消费者组的概念,消费者组中所有消费者是同一主题的消息的订阅者,同时kafka针对于消费者组在发送消息时,会将消息发送给所有该主题消息的消费者组,并且每个消费者组中只能有一个消费者进行消费。在这个基础上就能实现单播、多播、广播。
当同一主题的消息只有一个消费者组时,就能实现单播,因为消息只会发送到这一个消费者组,同时这个消费者组中也只能有一个消费者进行消费,这样就能实现单播
当同一主题的消息有多个不同的消费者组,且每个消费者组中消费者数量都大于1时就能实现多播,这个主题的消息会被分别发送到每个消费者组,但每个消费者组中只能有一个消费者进行消费
当同一主题的消息有多个不同的消费者组,且每个消费者组中只有一个消费者时就能实现广播,因为消息会发送到所有消费者组中,而每个组中又只有一个消费者
kafka生产者、消费者幂等性问题?
生产者幂等性问题
Kafka的生产者幂等性问题是指在正常的消息生产流程中,生产者生产消息后会发送到kafka中,kafka接收到消息后会将消息存到主题对应的特定的分区中,存放成功后Kafka会给生产者返回一个存放成功的信息,但实际项目中可能由于网络或者程序抛异常的原因,导致消息在成功存入分区后,Kafka没有及时返回这一个消息,这时候生产者就会认为消息没有存入成功,就会尝试进行第二次发送消息。这样分区中就会出现多条同样的消息。
解决办法:在Kafka中为每个不同生产者都设计了一个producer id(pid),同时在生成消息时会为每条消息生成一个递增的序号sequence number,Kafka在插入消息时就会携带这个sequence number和pid,在分区中插入数据前会先判断当前分区中对应的pid的最新消息的序号是否大于sequence number,如果大于就说明当前的消息已经插入过,直接返回插入成功消息,否则再进行插入
消费者幂等性问题
(参考mq的重复消费问题)
springcloud和dubbo的区别
dubbo是一个rpc调用技术的实现框架,如果要用dubbo实现微服务需要自己配置微服务的注册中心、网关、分布式事务等等,实现起来非常复杂
springcloud是一整套完整的微服务解决方案,其中已经封装了注册中心eureka、rpc调用框架fegin、zuul网关等等
dubbo和http的区别
dubbo是一个实现rpc的框架和技术,其中默认使用的通信协议就是dubbo协议,底层通过nio的方式进行实现的,dubbo除了支持默认的socket通信方式外,还支持多种其他的通信方式,包括了rmi二进制通信、hession和http,所以http作为一种通信协议是包含在dubbo的通信方式中的
redis应用场景
- 分布式锁,利用redisson的看门狗
- 缓存
- 购物车,用hash类型
redis的过期时间
redis中默认的过期机制是定期删除+惰性删除,定期删除是指redis会默认每100ms就在所有设置了过期时间的数据中进行抽取,当发现抽取到的数据已经过期就进行删除,但如果redis中存在大量设置了过期时间的数据,这个方法可能会使很多已经过期的数据一直存在,所以redis还有个惰性删除机制,即当用户在访问设置了删除时间的数据时,会先检查当前数据是否已经过期,如果过期了就会删除,再为用户返回空
redis内存淘汰机制
针对于redis的过期机制来说,如果数据库存在大量设置了过期时间的数据,同时这些数据用户也并未频繁访问,这样可能就会导致定期删除和惰性删除同时失效,导致redis中存在大量本应被删除的数据。这样的数据越来越多,最会就会耗尽redis的所有空间导致redis宕机。所以redis还针对这样的情况设置了内存淘汰机制,当redis内存消耗将达到阈值时就会调用。
redis中默认并未设置内存淘汰机制,当redis内存耗尽时就不能在向redis写入数据,并且会报错,一般常用的淘汰机制是最近最少访问和最不常用,最近最少就是在redis中删除最近最少访问的数据,最不常用就是删除访问次数最少的数据
redis单线程模型(redis如何做到单线程高并发的)
redis是将数据存储在内存中的,所以操作特别快,redis中有两个关键组件多路复用程序和事件分派器,首先多路复用程序用来监听所有socket,当发现有socket事件发生时就将这些事件压入队列中,让后队列会有序的每次仅一个将实现发送给事件分派器进行处理,事件分派器会判断当前事件是什么具体类型事件,比如当发现当前是连接事件就会将这个事件交给连接处理器处理,是读取事件就交给响应处理器处理,是写入事件就交给请求处理器处理
eureka注册中心工作原理
基于ap理论实现,使用eureka作为注册中心时,在启动注册中心时,首先eureka会自己设置一个定时器,每60s执行一次,用于删除90s还没向注册中心发送心跳的服务提供方,当服务提供方启动时,就会先去注册中心注册自己的信息,服务提供方有一个默认的续约机制,就是服务提供方每30s向注册中心发送一条信息表明自己还存在。在服务调用方启动时也会先去eureka中取得所有提供方信息,并保存在调用方,调用方也会每30s去eureka中更新一次最新的提供方信息列表缓存到自己本地
自我保护机制
当eureka发现短时间内有多台服务提供方都掉线后就会触发自我保护机制,不会讲将这些提供者从自己的列表中删除,而是先等待看这些节点是否能自动恢复
eureka集群(eureka如何保证高可用)
多个eureka进行集群时就是进行相互注册,即一台eureka会作为提供方缓存到其他的eureka列表中,当其中一个eureka有新的提供方进行登记时,随后也会同步到其他的所有eureka中
eureka和zookeeper区别(分别说eureka与zookeeper的原理)
(zookeeper原理)
zk是基于强一致性cp实现的,zk当中注册中心和调用者、提供者双方都是建立的长时间连接,也就是当调用者或者提供者信息方式变化时注册中心能够立即感知到并作出更新操作,这样做的优点是可以随时保证注册中心中的所有数据都是最新的,缺点就是不能实现高可用性,当zk中的数据由于网络原因不能实现同步时,zk就会关闭直到网络恢复正常。同理zk的集成也是如此,当集成的所有zk中若出现网络分区就会停止服务
eureka是基于高可用性ap实现的,eureka和提供者、调用者双方建立的连接都是短链接,也就是当双方的信息被修改时,注册中心需要延迟一段时间才能更新到,这样虽然不能保证强一致性,但可以实现高可用性,eureka不会因为数据不同步而被关闭。eureka的集群也是基于ap实现的,当出现网络分区时也不会直接关闭
分布式系统的cap理论
在分布式系统中, 无法避免会出现分区容忍性, 也就是节点之间可能会因为网络通信问题而出现故障, 导致出现故障的节点不能与正常的节点进行正常的信息通信, 这时就会出现分布式系统的分区问题. 只要是分布式系统都会出现这个问题. 而cap理论是指在有分区容忍性的情况下只能保证强一致性或者高可用性. 强一致性是指保证分布式系统中所有节点中的数据信息随时都是完全一致的. 高可用性是指分布式系统在任何时候被访问时都不会出现访问异常或访问请求被拒绝的情况.
如果选择保证强一致性(CP)就要保证分布式系统对外提供服务时不会出现分区的现象, 若出现分区就暂时停止对外提供服务, 直到分区问题解决. 如果选择保证高可用性(AP)就要保证分布式系统随时随地都能被正常访问, 所以当出现网络分区时, 就可能会出现数据不一致. 但为了实现高可用, 这时也不能对外关闭整个系统, 但在ap系统中一般会保证最终一致性, 也就是节点间的数据会间隔固定时间进行一次同步
redis事务机制
使用multi开启redis事务,之后的命令redis会依次放入队列中,将事务中命令写完后再执行exec提交,会依次执行队列中全部命令
redis持久化方式(备份机制)
两种备份机制,分别为rdb二进制和aof日志
rdb:默认开启,通过在配置文件中配置sava属性实现,比如配置save 60 10是指当redis中在60s内有10个key的数据发生改变时就进行一次备份,写入磁盘,这种方式不会消耗太多性能,但是风险比较高,比如如果在59s内有9条数据发生了改变但是redis宕机了,这9条数据就没有被保存
aof:默认关闭,可以配置每一次操作或每秒都进行一次持久化,虽然可靠性很高但很消耗性能
Feign远程调用的原理(FactoryBean的应用)
在使用feign进行远程http调用的时候,一般会将项目中每种Service业务会用到的远程调用的接口进行抽取,将其放入一单独的项目中,并针对这种远程调用接口项目整合feign,在接口上通过FeignClient注解标记当前接口对应注册中心中的哪一个服务提供者组,在接口中方法上通过Mapping注解标记当前方法在被调用时对应哪一个服务提供方controller的具体接口
//远程调用项目中的一个接口
//进行远程调用的GoodsFeignClient接口代码, 其他项目中在springboot入口类中通过EnableFeignClient注解来指定这个GoodsFeignClient接口所在位置, 就能直接在其他项目中注入GoodsFeignClient 接口, 底层就会根据下面的原理生成代理对象
@FeignClient("goods-service") //标记当前接口会调用注册中心中名为goods-service的服务提供者
public interface GoodsFeignClient {
//标记下面的方法在调用方被触发时对应提供方的哪个controller方法
@GetMapping("/goods/findGoodsById")
public GoodsDto findGoodsById(@RequestParam("id") Long id);
}
我们会在其他需要调用这个远程调用接口的项目的springboot的启动类上使用EnableFeignClient注解配置扫描, 配置完扫描后, springboot在启动时就会自动根据远程调用的FeignClient接口生成对应的FactoryBean对象并放入spring容器中,其中的getObjec方法就会创建并返回FeignClient接口的代理对象,所以我们能够直接在调用方通过Autowired获取这个FeignClient接口的代理对象,在进行远程调用时我们会调用其中的方法,触发代理对象的任何方法就会执行invoke方法,在invoke方法中,就会先获取被代理的FeignClient接口上注解中的提供者组名,再获取触发invoke的是哪个被代理对象的方法,并解析这个方法上面的进行远程调用的接口名,然后feign中集成了ribbon的负载均衡组件(包括的负载均衡算法有随机轮询和最小并发),就会根据已经的负载均衡算法选择具体的服务提供方进行远程调用,在调用时会feign默认会利用HttpConnection对象来向服务提供方发送http的调用请求,fegin也可以支持HttpClient来进行http调用,只需要在项目中引入HttpClient支持,再对其进行配置就能使用,HttpClient的http调用相比默认的HttpConnection的调用效率会更高
dubbo和feign的区别
dubbo是基于socket协议实现的,底层是利用netty中封装的nio通信方式建立的连接,并且dubbo与注册中心建立的连接属于tcp长连接。适合高并发情景,服务提供方一般是提供的service实现类,因为是针对service层的调用,所以不支持跨语言调用,dubbo默认使用的dubbo协议通信,也支持http、rmi、hession等方式进行通信。(nio的特点)
feign是基于http协议实现的,与注册中心建立的连接属于短链接,适合并发不高的情况使用,在传输信息时时使用的json文本格式,服务提供方提供的是controller接口,因为是建立在http上的调用,所以feing可以支持跨语言的远程调用。
网关的作用
- 统一前端接口
- 统一登录认证和权限鉴定(nginx做网关的话没有这个功能)
- 负载均衡
网关工作流程(nginx反向代理流程,网关在转发请求时是发送的http请求)
zuul网关或gateway网关挂了怎么办
用nginx集群所有zuul网关项目
Hystrix(实现)
通过熔断器保证不被下游服务拖垮,通过线程池或信号量隔离保证不被上游大量请求和外界环境压垮
熔断器
服务降级处理:下游服务超时可以降级处理在本地处理,在本地可以编写一个专门的备份方法,当发现下游服务不可用就能自动调用这个方法进行返回,降级机制会配合熔断器进行执行,熔断器一共有3个状态
- 默认情况下是关闭状态。即请求可以正常访问到下游服务
- 当一定时间内请求失败的数量达到了一定的比例就会触发熔断,熔断器完全打开,所有调用下游的请求都会直接被降级处理
- 半开状态:当熔断器完全打开一段时间后就会在调用下游请求前先验证一下下游服务是否已经回复,若恢复就关闭熔断器,若还是无法访问就切换回打开状态继续拦截所有请求降级处理
线程池隔离和信号量隔离
线程池隔离:若同时有大量请求访问到某个项目,但这些请求大部分都只访问项目中的一个接口,就会可能由于这个接口同时处理不了这么多请求被压垮,这样就会导致该项目其他的接口也不能正常使用,整个项目瘫痪。所以线程池隔离就是为每个不同的接口分别开启一个单独的线程池运行,避免被其他线程影响,保证每个接口的运行互不干扰,线程池隔离的优点是可以实现高并发,但缺点就是性能开销大,每个接口都需要维持一个线程池
信号量隔离:为每个接口设计固定数量的信号量,当有请求进入时将信号量-1,请求执行完后再+1,只有拿到信号的请求才能执行,若当前信号为0则拒绝访问,相比线程池隔离性能较低,但性能开销也没有线程池隔离大
SpringCloude中组件有哪些(注解有哪些)
- 注册中心Eureka:@EnableEurekaServer(注册中心项目使用),@EnableEurekaClient(服务提供方和调用方使用)
- rpc框架Feign:@EnableFeignClient(调用方使用),@FeignClient(远程调用接口使用)
- 熔断保护Hystrix:@EnableHystrix
- 负载均衡Ribbon
- 网关Zuul
SpringCloudAlibaba中组件有哪些(注解有哪些)
- 注册中心+配置中心Nacos
kafka分区写入策略
生产者在生产消息后会按照一定的算法将消息放入对应主题的特定分区中,分区写入策略包括了
- 轮询,默认策略,当消息没有key时会使用轮询策略
- 随机
- key值hash:消息可以分为key和value两个字段,所以kafka可以根据key值进行一定运算,根据运算结果选择特定分区放入
不同算法的利弊:
在使用key值hash时,我们可以将同一类消息的key值设置为同样的值,这样这类消息就会放入同一分区存放,因为这类消息是在同一分区存放的,所以可以保证消息的有序性,先生产的消息就会先执行,但缺点是如果某一类消息中包含了大量数据,就会出现数据倾斜,不同分区之间的数据量大小会出现很大差异。
在使用轮询和随机时,可以最大程度保证每个分区的消息数量是差不多一致的,但缺点是同一主题的消息会被分散在多个不同的分区中,这些分区中的消息在执行时就不一定会按照生产的先后顺序进行执行,所以在实际开发中要根据当前业务情况选择,若对消息的有序性要求高就用hash,否则可以用轮询或随机
kafka的消费者rebalance机制
总的来说当kafka中消费者组发生变化、消费者组订阅的主题发生变化就会触发rebalance机制,目的是为了保证消费者组中所有消费者都能均衡的负担所有消息。具体来说比如当消费者组中新增或删除了消费者、消费者新增或减少了订阅的主题数量、主题中新增或减少了分区数量都会触发这个机制。当出现以上状况时,整个消费者组将不会继续消费消息,而是会进行rebalance,直到rebalance完成后才会再继续消费消息,所以对性能的影响是比较大的
kafka的rebalance算法
- Range分配:kafka的默认策略,可以保证每个消费者消费的分区数量是大致相同的,算法定义n = 分区数量 / 消费者数量,m = 分区数量 % 消费者数量,比如当前主题有8个分区和3个消费者,那么n = 8 / 3 = 2,m = 8 % 3 = 2,则前m个消费者消费n+1个分区,剩余消费者消费n个,即前2个分析消费2+1=3个分析,最后一个分区消费n个即2个
- 轮询分配:即依次取出消费者组所订阅的每个主题中的每个分区,然后按顺序依次为每个分区指定消费者
- 粘性分配:当没有发生rebalance时粘性分配的策略和轮询类似,当发生rebalance时,会将新增的分区按照轮询的方式依次分配给已有的消费者,而不会针对之前为分区所分配的消费者进行改变,尽量使整个分配结果保持和rebalance之前一样
kafka的数据写入流程
生产者会首先根据注册中心获取到leader分区的位置,然后消息发送给leader分区,之后其他副本分区会主动从对应的leader分区中拉取信息并写入自己的 副本分区,最后根据不同的ack配置参数返回给生产者对应的ack信息
kafka的ack确认机制
针对ack参数的配置可以控制消息生产者在向副本写入消息时的严格程度:
- 当ack配置为0时,性能最高,但可靠性最低,生产者产生消息并放入leader分区后不会等待其他follower副本分区的写入确认,直接继续写入下一条数据
- 当ack配置为1时,性能和可靠性都是中等,生产者产生消息并放入leader分区后会等待收到至少一个副本分区的写入确认信息后才会继续写入下条消息
- 当ack配置为-1或者all时,性能最低但可靠性最高,消息被写入leader分区后生产者会等待收到所有副本分区的写入完成信息才会继续写入下条消息
kafka的数据消费形式
分为推模式和拉模式
- 推模式:消息的消费情况由消息队列进行记录,同时消息由消费队列主动发送给消费者,消息队列与消费者之间需要建立长连接,比较消耗性能,同时这种模式下消息只要被消息队列标记过就不能再被消费,代表比如rabbitmq
- 拉模式:消息的消费状态由消费者自己进行记录,并且每条消息由消费者自己从消费队列中拉取,这种模式下就可以自定义偏移量的位置来实现重复消费,比如kafka
kafka的数据消费流程
消费者会自己通过注册中心获取自己对应的leader分区的位置及其offset位置,然后根据这两个位置参数去消息队列中对应的分区中拉取offset坐标的消息进行消费,消费完成后再将最新的offset提交到注册中心进行保存
kafka的数据存储形式
主题是逻辑上的概念,一个主题可以划分为物理上的多个分区,一个分区又可以划分为多个段落(segment),一个段落又由多个文件组成,包括log、index等文件
kafka消息不丢失机制
broker数据不丢失
通过将消息同时写入leader分区和副本分区来保证broker数据不丢失,即使某broker崩溃后其他broker中也会有备份的分区数据
生产者数据不丢失
通过利用ack参数控制消息写入分区时的确认机制。。。。
消费者消息不丢失
消费者自己记录offset的位置,所以只要消费者做好每次消费后的offset的更新就能保证消费者消息不丢失,并且使用手动提交offset到注册中心的方式,避免自动提交过程中消费者宕机导致消息丢失
nacos命名空间作用
在大型项目中可以用来区分不同的开发环境下的代码和配置文件,比如针对开发、测试、运行环境可以分别设置对应的命名空间,其中存放每个环境对应的配置文件和代码,实现分类管理
nacos配置中心动态刷新原理
nacos配置中心结合了推模式和拉模式两种配置更新模式,首先是由客户端向配置中心发送一个请求,配置中心收到后会维持一个30s的长连接,在这30s内如果检测到配置文件发生改变就立即返回最新的配置文件到客户端,如果30s结束后没有发现配置文件改变也会返回一个没有变化的响应给客户端。客户端在收到配置中心的响应后间隔10ms又会再次发起一个请求到配置中心,这样循环下去就能实现动态刷新。这种模式既解决了推模式下配置中心需要一直维持长连接的性能消耗问题,又解决了拉模式下配置文件更新时效性较低的问题
zookeeper、eureka、nacos区别
nacos的原理:nacos既实现了ap由可以保证cp,首先为了实现ap,在nacos中也有类似eureka的心跳检测机制,服务提供方在注册后每间隔固定时间向注册中心发送一次心跳,同时如果提供方的信息被更新也会立即向注册中心发送信息,服务调用方每间隔固定时间从注册中心拉取最新的提供方列表。为了实现cp,在nacos注册中心与服务调用方间会建立基于udp的长连接通信通道,udp的长连接和tcp相比虽然可靠性较低,但性能可以大幅提高,当注册中心收到服务提供方所更新的信息后,就会通过upd通道将信息推送给消费方,消费方收到消息后再更新自己的提供方列表,就算在使用udp传输过程中可能会出现数据丢失的情况,nacos的心跳机制也能使消费方之后及时拉取到这个更新的提供方列表信息
nacos分别整合dubbo和feign的原理
nginx(网关)作用
- 统一访问入口
- 负载均衡
- 权限鉴定(配合lua脚本)
nginx应用场景
- 作为http资源的静态资源服务器
- 实现反向代理和负载均衡,当服务器并发很高时可以使用nginx对服务器进行集群,统一访问入口,并根据负载均衡算法选择服务器执行请求
- 配合lua脚本可以实现鉴权
kafka数据积压
原因一:业务代码原因,可能由于数据在写入数据库中时报错,导致消费者一直没有提交offset,导致数据积压,及时修改对应部分的bug
原因二:网络延迟原因,可能由于网络波动导致某一时间段内,消费者收到消息消费完成并签收的时间超过kafka设置的最大超时响应时间,这时针对超时响应时间进行调整即可
nginx怎么保证高可用高并发(挂了怎么办)
nginx可以利用lvs实现集群,lvs是一个虚拟的服务器,只要lvs中管理的nginx没有全部挂掉,lvs就能对外正常提供服务,其中lvs作为一级负载,nginx作为二级负载,两级都能实现负载均衡,集群nginx后就能实现高并发;
针对集群了nginx的lvs再配置keepalived,keepalived对外也是提供一个虚拟ip,在keepalived中配置两台lvs,一主一备正常情况keepalived把请求交给主lvs处理,当发现主lvs异常后再将请求交给备用lvs处理,主lvs恢复正常后再将请求交给主lvs处理
lvs挂了怎么办
f5硬件负载均衡
f5硬件负载均衡挂了怎么办
dns负载均衡
如何实现微服务的内部子服务与外网隔离?
物理隔离:子服务全部部署在内网中,对外只开放访问内网的网关
逻辑隔离:网关产生用于身份校验的token,各个微服务设置拦截器从网关请求中获取这个token,在进行内部子服务互相调用前再设置拦截器,将网关产生的token放入内部调用的请求request对象中,被调用者在拦截器中如果没有获取到token就直接拦截
微服务如何实现跨服务多表查询
- 使用api拼接,发起远程调用,缺点是效率低,如果有多个字段需要跨服务多表查询一次完整查询就需要发起多次远程调用
- 添加冗余字段,适合多表查询字段不多的情况
- 多数据源
- mycat:中间件
微服务中怎么实现统一服务操作多数据源?
配置多数据源
1. 分别注入多个不同数据源的连接池对象
2. 将所有连接池对象存入一个动态数据源对象DynamicDatasource中保存,mybatis或jpa使用的就是这个数据源
3. 编写自定义选择数据源的注解,这个注解可以作用在类或方法上,参数传递当前类或方法被调用时被操作的数据源
4. 编写aop通知,获取调用的方法或类上面的自定义数据源选择注解,将数据源参数取出来后绑定到当前线程
5. mybatis或jpa在操作数据库时,从当前线程取出在aop中绑定的数据源,并设置动态数据源当前操作的数据源对象为当前线程中绑定的数据源,发起对数据库的操作
数据库实现分库分表和读写分离
应用层实现:利用多数据源,优点执行效率高,缺点维护不方便,不能动态添加从库
比如可以利用aop+当前线程实现,先在项目中分别注入读表和写表的连接池对象,将连接池对象都放入动态数据源对象中DynamicDatasource,当service被调用时,aop会拦截当前执行的service方法,判断当前是读还是写,把对应的数据库连接池对象参数绑定到当前线程,在向数据库发起读写请求前,从当前线程取出所绑定的连接池对象,并找到动态数据源对象中所保存的这个对象,再发起请求
中间件实现:数据库与程序间使用中间件,在中间件中转发程序的读写操作到不同的数据库,如mycat
选择从表进行读取时要进行负载均衡
如果项目中有多个读表,在针对service中的查询方法进行调用时,我们还需要编写负载均衡算法选择一个读表执行查询
数据库实现主从复制原理
主库在进行写入操作时将操作过程写入主库日志文件中,从库会读取主库的日志文件,将主库日志拷贝到中继日志中,然后从库再重演中继日志中记录的操作,完成主从复制。实现时配置my.cnf的参数
数据库master节点挂了怎么办(mysql集群)
主主复制
设置两台主节点,一般一台读一台写,两台主节点设置双向的复制
级联复制架构
主节点负责写入,从节点负责读取,当进行复制时,先由主复制给两台从,其他从,再从这两台从进行复制,避免所有从同时针对主进行复制造成主节点的访问量过大
双主+级联复制
两台主节点负责写入,其他从级联复制
双主+keepalived
双主读写分离可以实现高并发,同时通过keepalived来进行双主集群,当其中一个主节点宕机后,keepalived可以自动切换故障节点保证高可用,当恢复后再回到双主分离模式
分库分表原则
- 垂直分片:不同微服务连接不同数据库(微服务的基本结构)
- 水平分片:当同一微服务所连接的库压力太大,就需要针对具体微服务的数据库再进行分片,即水平分片,根据具体情况选择分表或分库或分库分表(根据业务情况选择)
水平分从表:
当存储的数据量不大时,可以使用读写分离,设计多个从表专门用于读取,当主表写入数据时从表进行更新,通过水平拓展从表数量就能解决查询的高并发;
水平分主表:
当写入操作多到一定程度时,单个主表中写入存储的数据量过大,可以针对主表进行分表存储,按照某个字段的值进行分表存储,设计多个主表,分担存储压力
水平同时分主表和从表:
若写入操作数据量再继续增大,可以使用分库分表,在每个数据库中都设计主从结构
不同模式下分库分表的读写
- 客户端分片(水平分片):写入数据时利用雪花算法为每条数据生成唯一id,利用hash算法对hash值取数据库数量的模,根据结果选择特定数据库写入。查询时根据id计算hash值后找到对应的数据库
- 中间件分片:mycat,服务器连接中间件,由中间件实现具体分片,中间件再连接数据库,我们在mycat中操作逻辑库和逻辑表,底层在存储时就会自动帮我们把逻辑库和逻辑表拆分到多个mysql实例中
redis如何高可用高并发
高可用:主从复制+哨兵
redis主从复制:
主机可读可写,从机读,主机写入数据后保存内存快照,并将执行的命令缓存起来,快照完成后,主机会将快照文件和缓存的命令发给从机,从机再同步主机的内存快照中的命令完成同步
高并发:主从实现读写分离
redis主从读写分离:
redis主从节点可以实现主从复制,基于主从复制可以实现读写分离,这样不仅可以保证高可用还能实现高并发
redis哨兵
用于解决redis主机宕机问题
哨兵和主从机一样是独立运行的,哨兵会主动向主从机发送命令,等待主从机的响应,这样监控所有redis实例。
常用的哨兵模式Redis Sentinel针对哨兵也进行了集群,哨兵之间会相互监控,每个哨兵也会针对redis主从机进行监控,当哨兵监控到主机故障,会自动选举一台从机为主机,继续提供服务
redis集群脑裂
由于网络波动原因,使得主机与从机、哨兵集群之间断开连接,导致网络分区产生,这时哨兵会以为主机挂了,就会选择一个从机升级为主机,但此时客户端程序还在向原主机写入数据,当网络恢复后,新主机的数据就会覆盖原主机中的所有数据,造成数据丢失
解决方案:配置两个参数,一个规定主机连接的最少从机数量,一个规定从机连接主机的最大延迟时间,比如我们分别配置两个参数为3和10,代表只有在主机能正常连接3个以上从机,并且从机从主机同步数据的时间不能超过10秒的情况下,主机才能正常写入客户端的数据,否则主机会拒绝写入,以减少网络分区产生的问题
redis-cluster集群
主节点负责读写,从节点不提供功能,只负责故障转移
写入数据时采用虚拟分区算法,将所有集群后的redis主节点分为16384个槽,集群存储信息时会对key取模,得到的结果存入对应的槽上,程序将数据发给主节点时,由主节点先进行hash计算,根据计算结果选择卡槽存入数据,
缓存
参考:微服务架构改造——5
ehcache(特别高并发才会用到ehcahe做进程内缓存)
进程内缓存,每个用户在使用时会将数据缓存在自己本机上,不同用户终端之间的进程内缓存不能共享,效率比集中式缓存更高,Cacheable注解针对方法做缓存,根据请求参数缓存其执行结果、CachePut相比Cacheable可以保证缓存结果的同时每次方法都被执行
redis(一般项目只使用redis做缓存足够)
分布式缓存。或集中式缓存,针对用户访问量特别大的数据做集中式缓存,存放到redis中,各个不同的用户终端可以共享,效率比进程内缓存低
多级缓存架构工作流程
查询时先查询进程内缓存,再查询redis缓存,没查到再去数据库查询,查询猴数据写回两级缓存中
多级缓存架构下更新数据库,怎么解决其他终端中缓存不一致问题(mq的应用场景)?
多级缓存中,如果在一个终端修改数据,这个数据需要同步到其他所有终端的一级缓存以及二级缓存中,否则在其他终端就不能得到最新的数据,所以我们会用到mq发送同步消息,利用kafka实现多播,当一个终端数据发生更改,就会发送消息到mq中,其他所有消费者组的终端针对这个消息进行消费,更新自己的一级缓存及二级缓存,实现多个终端及缓存的最终一致性
redis缓存数据一致
先删缓存问题:
当一个客户端线程删除缓存后更新数据库之前,另一个客户端执行查询,此时又会从数据库查出更新前数据,导致缓存删除后马上又被其他线程读取出了和删除前一样的数据;
后删除缓存问题:
当一个客户端线程更新完后端数据库之后删除缓存之前,另一个客户端执行查询,此时读取到的缓存中的数据也是脏数据
一般使用延迟双删或延迟删除,双删就是一个更新数据库之前先删除缓存,更新数据库后延迟一段时间再删除,延迟删除主要是为了删除在第一次删除后更新数据库前其他线程写入到数据库的脏数据
微服务的网关鉴权和权限控制
day05
satoken
docker发布项目
- 在本地项目中新建src/main/docker目录,并在其中新建Dockerfile文件如下
# jdk基础环境
FROM openjdk:8-jdk-alpine
VOLUME /tmp
# 自定义镜像文件维护者信息
MAINTAINER xqy
# 设置环境变量-运行时也可传参进来耍哈
ENV JAVA_OPTS ""
# 添加jar包到容器中 -- tips: xx.jar 和 Dockerfile 在同一级
ADD *.jar /home/app.jar
# 以exec格式的CMD指令 -- 可实现优雅停止容器服务
# "sh", "-c" : 可通过exec模式执行shell =》 获得环境变量
CMD ["sh", "-c", "echo \"****** 运行命令:java -jar ${JAVA_OPTS} /home/app.jar\" & java -jar ${JAVA_OPTS} /home/app.jar"]
这个文件在制作镜像时会与jar包配合,共同完成镜像文件的创建
- 新建远程仓库,克隆到本地
- 本地写好的项目拷贝到本地仓库中,推送到远程仓库中
- linux中新建目录克隆远程仓库
- 进入拉取下来的项目文件中(pom文件一级),依次执行下列脚本
#将当前项目打成jar包
mvn clean package -DskipTests
#将打成的在target目录下的jar包拷贝到docker项目的docker目录中
cp target/ssm-0.0.1-SNAPSHOT.jar src/main/docker
cd src/main/docker
#将jar包结合dockerfile做成镜像文件,springboot:dev是自定义的镜像名字
docker build -f Dockerfile -t "springboot:dev" . --no-cache
#看当前有无正在运行的名为springboot的容器
docker ps -a | grep springboot | awk '{print $1}' | xargs -i docker stop {} | xargs -i docker rm {}
#创建并运行容器1,要注意项目中yml中配置的端口号要和:后面的docker内的端口号一致
docker run -e PARAMS='-Xms128m -Xmx256m' -p 8081:8081 --name springboot -d springboot:dev
#创建并运行容器1
docker run -e PARAMS='-Xms128m -Xmx256m' -p 8082:8081 --name springboot2 -d springboot:dev
docker-compose
当微服务的镜像不断增多,不同微服务之间还有启动顺序要求,启动时候就会很麻烦,所以可以引入docker-compose实现docker镜像的编排运行
使用方法:首先创建网络,再编写docker-compose.yml文件,其中就是本机中所有镜像文件的创建、容器的创建和运行的脚本代码,然后编写完成后直接运行这个脚本就能自动启动所有容器
参考:互联网冲刺二——docker部署——2
k8s
当所有容器都运行在单机上,直接使用dockers-compose就能实现项目启动,但是若容器分散在多个不同机器,就需要用到k8s实现跨机器部署docker容器,k8s中有一个主节点,其中不负责具体的容器管理,只负责从节点的管理,其管理的从节点就是运行docker容器的具体机器
Jenkins实现持续集成
参考:互联网冲刺二——docker部署——3
自动化部署,新建任务,配置git地址,编写shell脚本,点击构建即可自动完成从云端拉取代码、构建镜像、创建容器、运行容器一系列操作
分布式锁
用redis的setnx命令做分布式锁会造成死锁问题
redisson框架中在使用setnx基础上使用了看门狗机制
# ElasticSearch
ik分词器两种内置分词模式
- smart:最少切分
- max_word:最细粒度切分
自定义ik分词步骤
创建索引库
es的字段类型
- text类型支持分词,不能用group by分组
- keyword适合精确匹配,不能用于分词,可以用于long、double类型的分组查询
es的字段属性
- index用于设置当前字段是否可以被用于搜索
- store用于设置当前字段是否需要额外保存,默认为false
自动映射字段
手动映射字段
es的crud
es的匹配查询
match:全局分词匹配,会将搜索的内容进行分词,与库中设置了分词的字段匹配
term:精确匹配,可以匹配没有设置分词的字段
es的倒排索引
正排索引:根据id查询记录
倒排索引:以文档中分词后的单词作为索引,将包含该单词的文档id作为记录,建立倒排索引过程:插入数据时先生成正排索引,再对数据分词,根据分词和正排索引生成倒排索引
canel同步增量数据
eureka自我保护机制
数据库分库分表
原理:
- 垂直分表
- 水平分表,不同分表模式可以实现高可用高并发(多表复制原理)
- 多主结构:
- 每张主表存放不同的数据:两张表可以根据不同的条件分表存储不同的数据,或者根据雪花算法选择特定的表存放,同时读写,应对写入存储数据过大场景
- 每张主表存放相同数据,设置两两的相互复制,部分读部分写,实现读写分离,当部分节点故障可以让其他主节点同时读写,可以保证高可用和高并发
- 多从表结构:
- 级联复制结构:主表写入,从表读取,从表单向复制主表内容,从表使用级联复制方式减轻主表复制压力,适合高读取的并发场景
- 双主级联复制结构:双主写入,从表读取,双主双向复制,从表单向复制,双主保证高可用,多从保证高并发
- 分库分表:减轻数据库压力
- 多主结构:
- 实现方式:
- 中间件分表
- 客户端分表:spring多数据源实现-
设计一个购物秒杀系统
匿名内部类中为什么不能对局部变量进行修改?
匿名内部类中的i其实并不是局部变量i,而是根据局部变量的i所拷贝产生的被final修饰的另一个i,这样设计的原因:局部变量存放在栈空间中,当方法运行完这个空间自动被回收,而匿名内部类对象是通过new创建出来存放在堆内存中的,堆内存是靠jvm的gc机制进行垃圾清理的,所以在该方法执行完、匿名内部类对象被jvm回收前,其中的这个i就出现了安全问题,所以为了避免这个问题,jdk8就默认在匿名内部类对象中使用方法局部变量时,先拷贝一份,并将其用final修饰,将其存放到方法区常量池中,常量池中的变量不会因为方法结束而回收,这样就解决了这个问题
- nacos作为注册中心
- zk原理、dubbo原理、底层nio对比bio
- eureka原理、feign原理
- nac os原理
- nacos配置中心动态刷新
- redis集中缓存
- 缓存查询顺序
- 缓存雪崩
- 缓存穿透、击穿
- ehcache进程内缓存
- kafka多播实现增量多级缓存写入(单播、多播、广播)
- kafka数据写入策略
- kafka的rebalance机制
- 缓存使用mq双删策略(先删和后删的缺点)
- 分库分表
- spring aop实现动态数据源读写分离(事务管理)
- 动态数据源+自定义注解实现操作多数据库
- 水平分表
- 认证服务
- 用户端手机号一键登录
- 手机app端长时间登录
- 微信小程序一键登录
- 重复登录互斥
- 用户端手机号一键登录
- 网关认证(鉴权信息通过请求头传递给后续微服务)
- 后台权限控制jwt,鉴权下放微服务,aop+自定义注解权限控制