项目业务:
项目背景:
本项目主要针对与需要找工作的人员、招聘人才的公司以及培训机构
首先项目为找工作的人员提供合适的培训机构中的课程进行学习,可以进行线上以及线下学习,并且可以浏览平台中发布的招聘信息
然后为公司提供发布招聘信息、让员工学习指定的培训机构课程等服务,进行岗前培训等服务
最后为培训机构提供了发布课程,让各种人员进行学习等服务
为实现多方联动、互利共赢,从而开发了这套人力资源管理系统
项目架构:
首先项目整体分为6大模板:
1、课程中心:管理发布课程,课程的搜索服务
2、岗位中心:管理发布岗位信息,岗位的搜索服务
3、用户中心:系统用户的课程和申请和订单管理
4、鉴权中心:用户注册登录以及权限控制
5、机构管理中心:机构入驻与管理
6、系统管理中心:数据字典,员工角色权限信息的维护
然后项目整体采用前后端分离架构,前端使用vue技术栈,后端使用基于spirngboot+springcloud的微服务架构。
项目技术:
1、springboot搭建单个服务,springcloud治理服务
2、Eureka注册中心,用于服务的发现与注册
3、Ribbon/Feign,服务间的负载均衡调用
4、Hystrix断路器,保证微服务的健壮,防止雪崩
5、Zuul网关,微服务的统一入口
6、配置中心,将配置文件统一管理
项目中遇到技术点及问题:
跨域问题:
浏览器自动发送两次请求,第一次发送option请求做询问,第二次发送正是请求,后端配置跨域过滤器,通过过滤器往响应头添加允许访问的请求响应头就可以解决此问题,项目中在网关配置了过滤器
然后跨域问题就是因为不同Ip、端口互相访问造成的,只有前端在发送ajax请求才会出现跨域问题
解决方案:
在网关中配置允许跨域的地址以及允许的请求方式和允许请求头消息等
租户入住业务:
如果操作的domain涉及到了关联查询,就要在那个domain里面加入新的关联对象,并且在关联对象上面打上注解@TableField(exist=false),用于在生成sql时忽略该字段
后台覆写service的实现进行保存
首先租户注册参数包含其他表的字段,所以需要单独将租户注册页面的所有字段封装到实体类中
然后再service实现类上添加 @Transactional注解,表示事务管理,操作多张表时要么都成功,要么都失败,
在进行对租户信息保存时,首先需要向租户表插入数据,mybatis默认会返回新增数据的主键Id,并且需要设置租户的状态以及注册时间为当前的时间
然后在对员工表进行操作,需要将租户注册页面的用户名以及密码插入到员工表中,还需要将租户的Id 设置为员工表中的租户id
再然后每一个租户的管理员购买不同的套餐具有不同的权限,所以需要设置租户表中租户管理员的Id,将当前注册用户名ID设置为租户管理员ID,设置时可以先查询到数据后再进行修改,这样可以防止修改的时候数据丢失问题
最后因为租户购买了套餐,所以还需要对租户和套餐的中间表进行设置,中间表包括租户ID、套餐ID,以及超时时间,可以直接获取租户以及套餐的ID设置进中间表中,由于租户表本身是不具有套餐ID的,所以需要从封装的Vo实体类中获取套餐的ID,最后设置超时时间,获取当前的时间然后+1年,然后在获取毫秒值后设置到中间表中
Fastdfs的架构原理:
FastDFS 是用c语言编写的一款开源的分布式文件系统。充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker
server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。
Tracker
server 作用是负载均衡和调度,通过 Tracker
server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器,,tracker 也可以实现集群。每个 tracker 节点地位平等。收集 Storage 集群的状态。
Storage
server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器,Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。
使用fastdfs实现文件上传的流程
客户端上传文件后存储服务器将文件ID返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。
首先组名就是文件上传后所在的storage组的名称,在文件上传成功后由storage服务器返回
然后虚拟磁盘路径就是storage配置的虚拟路径,与磁盘选项 store_path*对应。如果配置了store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。
再然后就是数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。
最后是文件名,与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器,IP地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息
课程类型树菜单的遍历问题:
首先在项目中查询无限级别的课程类型的时候,最开始采用的是递归的方式,当时发现每一次递归都要查询数据库,效率低,对数据库的操作太频繁了,并且如果递归的深度太深,可能会导致栈溢出,所以后面优化采用了循环+Map的方式
而使用循环+map的方式可以有效率的查询到数据,并且对数据库的操作减少了很多。
获取无限级别的类型树业务
首先创建一个list集合用于存放所有课程的一级类型并查询出所有的课程类型
然后将查询出来的所有类型的ID作为KEY,类型封装的对象作为Value存入map
再然后遍历所有的课程类型后进行判断,如果是一级类型,将值存入list中,如果不是一级类型,则获取课程类型的父ID(这里如果使用的是list,就需要再次遍历【嵌套循环】,效率较低,所以使用了map,就可以直接通过类型的ID获取对应类型的父类型,效率较高)
最后将当前类型添加到父类型的子类型集合中,最后返回这个list集合
课程类型是以什么数据类型缓存到reids中
可以选择字符串类型和序列化
首先java中的对象如果要存入redis中可以选择String(字符串),将java对象转换成json字符串,保存到redis中,从redis查询出来再将json字符串转换为java对象
然后可以选择序列化(byte[] 字节数组),如果对象比较大,并且里面有很多属性之外的方法要保存,json字符串是不合适的,所以可以选择对象序列化成字节数组保存到redis中,从redis中查询出字节数据,再反序列化成java对现象就可以使用了
可以选择字符串,如果json字符串比较长,可以将继续序列化成byte[]字节数组
将课程类型缓存到redis中的原因
课程类型很少修改,经常查询将课程类型,所以需要缓存到redis中,就是为了减轻数据库的压力,而不需要对数据库进行频繁的操作,并且可以提高访问速度,增强用户的体验
缓存穿透
首先缓存穿透是因为某些人如果发送大量的请求访问id不存在得用户信息,在缓存中查询不到,会直接查询数据库,如果并发量很高,这种现象就是直接穿透了redis缓存,大量请求之间访问数据库会使数据库拉跨,造成数据库宕机
然后解决方案就是使用布隆过滤器亦或是如果缓存不存在这个数据,则继续访问数据库,当数据库也查询不到此数据时,则存储一个null值再redis中,并设置过期时间,同时使用同步的方式避免高并发情况下还是有很大一部分请求会直接访问数据库的情况。
缓存击穿
首先缓存击穿是因大量的请求同时访问某一个KEY,而这个KEY由于刚刚过期或者是redis刚刚启动缓存中还没有此KEY,造成大量的请求访问数据库的情况
再项目中使用了同步代码块和双重校验锁的形式进行对数据的查询,可以有效地避免大量的请求同时访问数据库的情况。
缓存雪崩
首先缓存雪崩是因在同一时间大量的缓存失效,导致查询这些缓存的请求都会访问数据库,造成数据库压力过大,和缓存击穿不同的是,缓存击穿是热点数据失效,某几个经常访问的key,而缓存雪崩是大量的key同一时间失效,比如是在项目启动时,从数据库中加载所有的用户信息,设置了相同的过期时间,时间到后,这些用户信息都会过期,查询用户的请求都会访问数据库
解决方案就是让过期时间均匀分布(过期时间和随机值配合,在一段时间内一次过期)
使用ElasticSearch的原因:
首先前台搜索课程的时候,会通过课程名称等进行模糊匹配,如果课程非常多,like '%xx'会导致索引失效,造成全表扫描,并且效率较低
然后再高并发场景下,如果所有的课程的搜索都是访问数据库,会导致数据库宕机,所以不适合再高并发场景中使用
所以会使用ELASTICSEARCH,简单说Elasticsearch使用Lucene作为内部引擎,但是在使用它做全文搜索时,只需要使用统一开发好的API即可,而不需要了解其背后复杂的Lucene的运行原理。并且可以让数据都保存在ES中,前台用户搜索数据时只能从ES中搜索,这样就对数据库的好处是十分大的,并且ES的分布式特点支持海量的数据搜索
课程的上下线业务:
上线:
首先客户端发送请求,执行课程上线,请求到达后台后执行查询数据库,将上线的课程数据中查询出来后,调用ES的批量保存接口,将数据保存到ES中,ES中只会保存上线的课程,并且修改数据库中的上线时间和状态,用于前端显示
下线:
首先前端发送课程下线的请求到达后端时调用ES接口,将数据从ES中删除,并且与数据库中的数据进行同步,修改数据库中的下线时间和状态,修改和删除只能操作下线的课程,如果该课程处于上线的状态,则应该先进行下线的操作。
使用静态化页面时遇到的问题:
使用feign进行文件的上传下载,首先写了一个配置类用于文件上传编码,因为文件上传必须使用到编码,并且在类上添加了@configuration,使这个编码配置类变成整个项目都在使用这个编码了,导致在调用Feign接口时,map的编码出现问题了,解决方法就是把@configuration去掉,这样就只是把编码单独配置给了Feign作为文件上传和下载的配置
问题二:
使用feign上传静态化页面后返回的fileId为空,将托底数据注释后发现报错为time out and not fallback,初步判断为消费者调用服务超时,修改消费者熔断配置后,解决问题
静态化页面业务流程
首先前端调用课程首页页面静态化接口,然后课程服务将模板打包上传到fastdfs后返回fileId,再然后课程服务准备课程类型的data数据后调用页面静态化接口,传入返回的fileId以及准备的数据,根据模板压缩包的fileId从fastdfs中下载模板的压缩包并解压后设置data中的staticRoot数据(模板的根路径),然后调用工具类的方法进行页面静态化的生成后将文件上传到fastdfs中,fastdfs会返回文件的fileId,将fileId相应给课程服务然后发送到rabbitMQ中,使用部署的java项目(agent)监听rabbitmq的队列,获取fileId载根据fileId下载静态化页面放入前端项目中,并且在增删改课程时同步重新生成课程首页
短信验证码业务
首先获取请求中的uuid和imageCode,判断图形验证码是否正确,如果错误则返回图形验证码错误消息,如果正确则进行判断redis中的短信验证码是否存在,如果不在证明没有发送过或者前一次发送的短信验证码已经超时了,重新生成短信验证码并保存到redis中,然后调用短信发送接口将验证码发送,如果redis中的短信验证码已存在,则获取上一次发送的时间,然后判断是否已经超过重新发送的时间,如果没有超过则是非法请求,不再发送短信验证码,如果时间超过则将上一次发送的时间设置为当前时间再调用短信发送接口将验证码发送出去
单点登录的解决方案
有三种方案,可以依赖于一些权限框架,比如说shiro、security作为认证框架,还可以使用单点登录的服务器CAS,最后可以自己设计
单点登录业务实现
首先前端用户访问需要认证的服务时进行判断是否登录,如果没有登录则调用授权中心给前端用户响应一个登录页面进行登录。
然后前端用户发送登录请求后进入网关中,网关对请求进行过滤放行登录请求,然后路由到授权中心进行登录的操作,登录成功后将用户信息(access_token)存储到redis中,用户信息以key,value的形式,,key设置为随机值,value为用户的信息,然后将access_token保存到cookie中,前端就可以通过cookie获取到redis中的用户信息了。
最后登录成功后浏览器在进行跳转的其他页面时,页面判断浏览器的cookie中的access_token是否存在,并且验证access_token是否正确,如果正确则将页面返回给前端用户,如果不存在或者错误则再次调用授权中心将登陆页面返回给前端用户
如果是访问后端的单点登录,过滤器拦截请求,获取请求中的access_token,判断redis中的access_token是否存在,如果存在则说明登陆过,放行,如果不存在则表示没有登录过,进行拦截
网关的作用
首先网关作为微服务的统一入口,为所有请求统一做过滤
然后实现单点登录的时候,是在网关中配置过滤器,当前端发送请求时,可以查看请求中的access_token,并且可以验证access_token的正确性,以实现达到单点登录的目的