一手讯
一手讯是一款新闻资讯类的app,主要为用户推送一些热点新闻资讯内容,通过搜集用户的行为数据,分析用户的行为特征,为用户提供感兴趣的新闻资讯,同时为自媒体人提供了自运营的平台,自媒体人可通过平台来发布自己的文章资讯
负责的功能模块
用户的登录》自媒体人文章的发布》文章延时处理》文章审核》文章保存到app端》文章生成静态页面保存到minio
1、用户登录
有两种用户身份,游客和普通用户,普通用户在注册账号时会输入账号和密码,在数据库表中添加这个注册的用户信息,注册时会生成一个随机的字符串,叫盐字段,通过用户注册时输入的密码和盐字段进行拼接在进行md5加密之后得到最终的密码保存到数据库表中,
注册好的用户通过账号和密码进行登录时,先拿用户的账号在数据库中查找看该用户是否存在,如果不存在返回账号错误提示,如果账号存在在进行密码的比对,将用户输入的密码与数据库中该用户的盐字段进行拼接加密之后的密码与数据库中保存的密码进行比对,比对失败,返回账号或密码错误,比对成功用户进入首页面
游客身份登录不需要账号密码,权限相对较小,只能查看不能点赞评论收藏
2、jwt效验
用户发送的请求会先进入网关,网关中的过滤器判断该请求是否是登录请求,如果是登录会路由到登录的微服务中,如果不是,在判断该请求中是否包含token令牌,没有token,重新路由到登录服务,因为登录之后才能获取到token令牌,如果存在token令牌,在判断该令牌是否过期,过期重新路由到登录服务,没有过期,在路由到具体的微服务上,
3、自媒体人文章发布
自媒体人在编辑完文章内容后需要进行提交或保存草稿,然后会在数据库中的news文章表中查看该文章的id是否存在,如果存在id,为修改操作,如果没有id为新增操作,先假设没有id,会将文章内容保存到文章表中,在判断该文章是否为草稿,在提交的数据中有一个static字段表示文章状态,0位草稿,为草稿结束请求,返回操作成功结果,不是草稿,需要保存文章和文章和文章素材图片的引用关系到关系表中;如果在数据库表中查询存在此文章id,即为修改操作,需要先删除文章与素材图片的引用关系,在在文章表中进行文章内容的修改,修改完之后在判断他是否是草稿,来决定是否保存文章与图片素材的关系
文章中的图片以及封面的图片,我们是通过调用minio,将图片上传到minio进行保存,数据库表中记录的是图片访问的地址路径
4、文章的延时功能
文章数据在自媒体端保存完成之后,通过调用feign的远程接口,将文章保存到延时任务模块中,延时模块接收到数据之后会先保存到数据库,然后在定时5分钟刷新一次,检查文章发布时间,如果是当前时间保存到redis中的list队列中,如果文章的发布时间是在当前时间与当前时间加5分钟之后的未来时间(预设时间),存入到zset队列中,然后zset队列中存入的未来文章会每分钟刷新一次,检查发布时间是否到达当前时间,到达当前时间在添加到list队列中,list队列中保存的文章数据每秒会通过fegin,向文章审核功能的模块去发送文章数据进行审核
在成功将list中的数据发送到文章审核功能时,需要删除调数据库表中和redis中保存的推送过的文章数据
定时功能的实现:在pom中引入task依赖,在需要开启定时功能的方法上标注scheduled注解,注解属性中用cron表达式来指定定时时间,在启动类上开启定时任务的支持,标注enableScheduling注解
自媒体人取消任务时,会将延时任务模块中的文章数据删除掉,
当在微服务中有多个延迟任务的服务时,可能会出现一个任务多次消费,所以我们要使用互斥锁,redis的互斥锁是这样实现的,用到了一个setnx这样一个命令,在指定的key不存在时,为key设置value值,如果key存在,则setnx命令不做任何操作,
redis实现分布式锁的大致流程:首先客户端请求服务器设置key值,如果这只成功表示加锁成功,那客户单在设置这个key时它已经有值了就会设置失败,就相当于没有获取到锁,等到客户端执行完之后,删除调设置的key数据,相当于释放锁,那客户端b在等待一段时间后再去请求这个key值,设置成功获取到锁,就获取到代码的执行权了
还有一点,在redis中的zset队列中保存的未来数据定时刷新到list队列中时,需要在redis中批量查询未来时间的数据,在批量查询时,一般用的是keys命令模糊匹配,但是这个命令在生产环境下会导致redis对CPU的使用率极高,因为redis是单线程的,会被堵塞;还有另一种方法,使用scan命令,scan命令是基于一个游标的迭代器,每次调用完scan命令时,游标会向下走一格,返回一个新的游标位置,用户下次会在通过这个新的游标指向的位置去访问数据
5、文章的审核
在延时任务中,文章发布时间到达当前时间时,会通过feign接口,调用文章的审核接口,文章的审核在自媒体端完成
延时任务会发送过来一个文章id给文章的审核功能,通过文章id,在自媒体端文章表中获取到该文章,将文章中提取纯文本内容和文章中的图片和封面图片进行审核
首先是敏感词过滤,是我们自己维护了一套敏感词库,保存在数据库表中,通过DFA算法,也叫确定又穷自动机,是一种数据结构,在进行敏感词查询时,会先将表中的所有敏感词存储到一个嵌套的map中,比如词库中有一个冰毒的敏感词,会将冰字作为key存入,在value的位置在创建一个map,这个map的key会存入毒字,它对应的value会存入一个isEnd=1的属性,在以冰为key对应的value上现在已经创建一个键值对(毒:isEnd=1)了,现在冰毒这个词语结束了,那会在这个冰对应的value上创建第二个键值对表示该敏感词结束了,存入(isEnd:0)这个键值对,以这种存储方式进行敏感词的初始化,当isEnd的值是1时,就表示匹配到了敏感词,反之isEnd是0时,就表示没匹配上敏感词,举个栗子:比如说我们要对“我不买卖冰毒”进行一个敏感词过滤,会先遍历这一句话,得到每一个字符,然后拿每一个字符在敏感词库中去比对看是否存在,如果存在就查看该字对应的集合,获取isEnd属性值,如果敏感词库中不包含此字符就直接结束,表示没有匹配到敏感词,最终审核的结果会封装到一个map<String,Integer>中,如果map中有内容(map.size>0),map的key会记录匹配到的敏感词,value会存入对应敏感词出现的次数,就表示文章中有敏感词,相当于审核失败,会在自媒体端的文章表中修改文章的状态值为2,表示审核失败,并给出审核失败的描述信息,可以把map中的key拼接上
其次是文章内容中的纯文本审核,调用的是阿里云的审核接口,需要注册一个ak账号,然后开通文本和图片审核的服务,在文章表中保存的文章内容类型是一个longtext类型的大文本文件,将当于是一个中括号里面保存了多个大括号的对象,每个大括号里面有两个键值对,第一个键值对的键是type表示文章中内容的类型,如果对应的value是text就表示文本,如果是image就表示图片,第二个键值对的键的名字是value,对应的value的值看第一个键值对写的什么,是text那么第二个键值对的value位置就写文本的内容,是image就写图片的访问路径,通过解析这个文章表中的文本内容,把纯文本和图片分开,分别封装到不同的集合中,用于审核时不同数据类型的提交。将封装好的文本内容调用阿里云文本审核方法进行审核,返回审核结果,通过,失败,待人工审核,如果审核失败,再次结束方法,在文章表中修改文章状态为审核失败或待人工审核状态对应的数字,并在表述字段上给出审核未通过的理由,如果审核通过在进行下一步的审核
文本审核通过之后,在进行图片审核,文章表中有两个字段记录着图片的访问地址,一个是文章内容中的图片,一个是文章封面图片,将两个字段的图片访问路径都获取到,调用minio的图片下载的方法,获取图片数据,在调用阿里云的图片审核方法,对图片进行审核,审核结果和处理方式和文本审核一样
在完成图片审核之后,我们需要对图片中的文字进行审核,这里用到了一个OCR图片文字识别技术,需要导入一个tess4j的依赖,这个技术支持一百多种语言,我们只识别中文,所以只导入中文词库就可以了,在需要指定语言为chinese,在调用响应的api,传入图片数据,返回一个String就是识别到的图片中的文字,然后在通过之前用到的阿里云文本审核的方法,在对这些检测出来的文字进行审核,假设审核失败,处理结果同上,审核成功,会将文章通过feign的远程接口,将文章保存到APP端,并返回文章id,然后在自媒体端将文章的状态改为已发布状态,并将调用feign接口后返回的id保存到自媒体端的文章表中
6、app端文章的保存
在app端中有3张表,文章表,文章配置表 和 文章内容表,我们通过feign发过来的文章数据会分别保存到这3张表中,文章表记录的是文章的基本数据,文章的标题、作者id姓名、文章频道、文章布局、标记、图片等一些常用到的且占用空间不大的数据,配置表中保存的是文章是否可评论转发,是否下架或删除,一般新发布的文章会添加默认值,文章内容表中保存的只有文章的内容,因为是大文本类型数据,所以把他单独放到了一张表中,
这3张表是一对一关系,是根据功能和大小差分成了三张表,通过文章ArticleId主外键进行连接,这里会存在一个问题,如果3张表的主键id都设置了自增策略,那就会产生重复的id,这里我们是用分布式id:雪花算法解决的,我们持久层用到的是mp,mp已经继承了雪花算法,我们只需要如下:
如果自媒体用户在自媒体端进行的是修改文章的操作,通过延时、审核之后会到APP端,在app端中的文章表中查看发过来的这个文章数据是否包含文章id,包含id就是修改,反之就是新增文章
7、生成静态文件
在完成app端的文章保存之后,我们还需要将app端的文章内容表中的文章内容,通过Freemarker模板技术,生成一个静态的html页面,在将这个静态页面保存到mino中,返回的静态页面文件访问路径保存至文章表中,app端用户登陆成功最开始展现的是静态页面的数据,文章的标题发布时间作者和一些封面图片,当用户觉得这片文章不错,点进去之后才会查询文章内容,通过Freemarker生成的静态页面,可以直接访问minio获取,减少了数据库的访问,同时也减少了不必要的文章内容的加载,用户点击查看时在加载,
模板是前端提供给我们的一个文件,我们只需要将数据填充到模板中的数据模型上即可
8、将文章保存到ElasticSearch
在 app端完成数据保存之后,需要将文章数据通过kafka发送到searh微服务中,就是es文章检索这个模块中
用户在app端进行文章的关键字查询,联想词扩展,查询字段高亮显示,条件查询等这些功能我们用es来实现,es相较于数据库来说,对于海量的数据查询更快捷
1、我们首先需要在远端拉取es的镜像,然后基于镜像创建容器,配置ik中文分词器
2、创建索引库,指定多个文档名称和类型,比如:id:type:long、publishTime:type:date
3、先将首批的数据导入到索引库中保存,如果数据量过大需要分页导入,先查询符合条件的文章,比如是否删除是否下架,将查询到的满足条件的文章对象封装到一个list集合中,在进行批量导入,需要new一个bulkRequest对象,指定索引库名,遍历list对象,得到每一个文章对象,在new一个indexRequest对象,添加文档id和文档内容,id从文章对象中获取,文档内容就是josn格式的文章对象内容,在用bulkrequest对象的add方法添加indexRequest对象,最后调用es的客户端对象restHighLevelClient.bulk(bulkRequest,RequestOptions.DEFAULT)来完成数据的批量导入
4、批量保存完数据后,在完成文章的搜索功能, 用户在进行关键词搜索时,会用到mangoDB来保存用户的搜索记录,在用户点击搜索框时以列表的形势展现出来,随着后期应用上线,用户数量可能会随着时间的增长而增长,需要存储的空间会越来越大,且mongdb的访问速率相对于数据库是很快的,但他有个缺陷是不能保证数据的安全性,也就是可能会丢失一部分数据,所以用户的搜索历史记录相对而言不是特别总要的数据,允许数据少量的丢失而不会造成太大的影响
现在用es实现文章的搜索,首先需要创建一个SearchRequest对象并指定索引库名,然后在创建一个boolQueryBuilder对象,来封装多个条件,搜索的条件有:关键词分词之后的查询,根据文章的发布时间来筛选文章,分页查询,按照发布时间倒序排列,高亮显示关键字,通过多条件查询到的数据,将数据封装到一个list<Map>中,解析结果数据,将原始标题解析出来,用高亮标题去覆盖原始标题完成高亮显示,最后将处理好得到数据封装到list中后返回给页面
5、在保存用户搜索记录是使用的是异步调用,@Async和@EnableAsync,来提高给用户的响应速度
6、es为什么快呢?es使用了倒排索引,倒排索引也可以看做是一张表,表中有词条和文档id两个字段,在向索引中存入数据时,会先将文档中的所有内容分成词条存入到倒排索引中的词条字段中,词条是唯一的不能重复,每一个分出来的词条会对应一个或多个文档id,因为词条是唯一的,我们可以基于词条字段来创建b+tree索引,将来我们根据词条来查找的速度就非常快了
正向索引:基于id创建的索引,它在检索的时候,如果搜索的是非索引字段,必须逐行扫描进行检索,然后进行匹配,先找文档,然后在判断文档是否包含词条
倒排索引:先对内容分词得到词条,然后基于词条创建索引,记录词条所在的文档的信息,查询的时候,是先根据词条查询到文档id,然后在根据文档id获取到文档