当前位置:网站首页>仿牛客论坛项目梳理
仿牛客论坛项目梳理
2022-08-04 03:02:00 【三月不灭】
敏感词算法对文章内容进行过滤
敏感词存放在指定文件中,建立前缀树时会扫描该文件每个词,将每个词拆分成一个个字符分别存储在前缀树上,第一层包含所有敏感词的第一个字母,第二个敏感字符存在第二层,每个节点上,从根节点的孩子节点到叶子节点途径的这个字符连起来就是一个敏感词,叶子节点会有一个标记,到这就是一个敏感词结尾
begin指针指向过滤的目标字符串的开头
首先有三个指针, position指针的作用是指向待过滤的字符,若position指向的字符是敏感词的结尾,那么
text.subString(begin,position+1)就是一个敏感词
tempNode指向前缀树中当前扫描到的节点,每次在目标字符串中找到一个敏感词,完成替换之后,都要再次从根节点遍历树开始一次新的过滤
扫描一个文本时,begin指针和position指针都指向首字符,查询是否有一个字符开头的敏感词,如果tempNode=null,没有,则position和begin都往前移动,如果tempNode != null,则存在以该字符开头的敏感词,position指向往前推进,遍历下一个字符,若下一个字符不在tempNode的孩子节点中,则begin到position不是一个敏感词,则遍历begin下一个字符;如果在tempNode孩子节点中,且该节点是叶子节点,则begin到position使用给敏感词,进行替换,然后遍历position下一个字符;如果在tempNode孩子节点中还没到叶子节点,则position移动到下一个字符遍历,依次进行。
public String sensitiveWordFilter(String text) {
//若是空字符串 返回空
if (StringUtils.isBlank(text)) {
return null;
}
// 根节点
// 每次在目标字符串中找到一个敏感词,完成替换之后,都要再次从根节点遍历树开始一次新的过滤
TrieNode tempNode = rootNode;
// begin指针作用是目标字符串每次过滤的开头
int begin = 0;
// position指针的作用是指向待过滤的字符
// 若position指向的字符是敏感词的结尾,那么text.subString(begin,position+1)就是一个敏感词
int position = 0;
//过滤后的结果
StringBuilder result = new StringBuilder();
//开始遍历 position移动到目标字符串尾部是 循环结束
while (position < text.length()) {
// 最开始时begin指向0 是第一次过滤的开始
// position也是0
char c = text.charAt(position);
//忽略用户故意输入的符号 例如嫖※娼 忽略※后 前后字符其实也是敏感词
if (isSymbol(c)) {
//判断当前节点是否为根节点
//若是根节点 则代表目标字符串第一次过滤或者目标字符串中已经被遍历了一部分
//因为每次过滤掉一个敏感词时,都要将tempNode重新置为根节点,以重新去前缀树中继续过滤目标字符串剩下的部分
//因此若是根节点,代表依次新的过滤刚开始,可以直接将该特殊符号字符放入到结果字符串中
if (tempNode == rootNode) {
//将用户输入的符号添加到result中
result.append(c);
//此时将单词begin指针向后移动一位,以开始新的一个单词过滤
begin++;
}
//若当前节点不是根节点,那说明符号字符后的字符还需要继续过滤
//所以单词开头位begin不变化,position向后移动一位继续过滤
position++;
continue;
}
//判断当前节点的子节点是否有目标字符c
tempNode = tempNode.getSubNode(c);
//如果没有 代表当前beigin-position之间的字符串不是敏感词
// 但begin+1-position却不一定不是敏感词
if (tempNode == null) {
//所以只将begin指向的字符放入过滤结果
result.append(text.charAt(begin));
//position和begin都指向begin+1
position = ++begin;
//再次过滤
tempNode = rootNode;
} else if (tempNode.isWordEnd()) {
//如果找到了子节点且子节点是敏感词的结尾
//则当前begin-position间的字符串是敏感词 将敏感词替换掉
result.append(REPLACE_WORD);
//begin移动到敏感词的下一位
begin = ++position;
//再次过滤
tempNode = rootNode;
//&& begin < position - 1
} else if (position + 1 == text.length()) {
//特殊情况
//虽然position指向的字符在树中存在,但不是敏感词结尾,并且position到了目标字符串末尾(这个重要)
//因此begin-position之间的字符串不是敏感词 但begin+1-position之间的不一定不是敏感词
//所以只将begin指向的字符放入过滤结果
result.append(text.charAt(begin));
//position和begin都指向begin+1
position = ++begin;
//再次过滤
tempNode = rootNode;
} else {
//position指向的字符在树中存在,但不是敏感词结尾,并且position没有到目标字符串末尾
position++;
}
}
return begin < text.length() ? result.append(text.substring(begin)).toString() : result.toString();
}
// 判断是否为符号
private boolean isSymbol(Character c) {
// 0x2E80~0x9FFF 是东亚文字范围
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
// 前缀树
private class TrieNode {
// 关键词结束标识
private boolean isKeywordEnd = false;
// 子节点(key是下级字符,value是下级节点)
private Map<Character, TrieNode> subNodes = new HashMap<>();
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
// 添加子节点
public void addSubNode(Character c, TrieNode node) {
subNodes.put(c, node);
}
// 获取子节点
public TrieNode getSubNode(Character c) {
return subNodes.get(c);
}
}
使用Elasticsearch做全局搜索
相关知识点介绍
全文检索: 不同于数据库的检索,全文检索是对一段文本进行切分,切分出来的关键词建立索引,并记录了索引在文章中出现次数和位置。日后查询也是基于关键词建立的索引来查询,查询的结果是有相关度排序的,越靠前记录相关度越高。数据库的查询,模糊查询就相当于一条一条的去找,找到符合条件的记录返回,速度慢且无相关度排序
ElasticSearch: 一个分布式的、 Restful风格的搜索引擎,检索速度快,支持对各种类型的数据的检索。可进行过滤,聚合,分页,关键词,范围,前缀,通配符,模糊,多id,多字段,高亮等查询,或者排序。
Elasticsearch底层分别为每个字段都建立了一个倒排索引,索引里分为两个区,一个叫索引区,一个叫元数据区,索引区存放对文档分词之后关键词所对应的索引id,和关键词在文章中出现次数和位置,可用于计算文档相关度对文档进行排序。元数据区存放原始文档。搜索时es会先在索引区进行检索,找到符号条件的id,在根据id去元数据区查找对应数据。
在ES中除了text类型分词,其他类型不分词,保留完整内容。
分词通过分词器完成,把全文本转换一系列单词,每个单词都指向他所在的完整文档
内置分词器
- Standard Analyzer - 默认分词器,英文按单词词切分,并小写处理
- Simple Analyzer - 按照单词切分(符号被过滤), 小写处理
- Stop Analyzer - 小写处理,停用词过滤(the,a,is)
- Whitespace Analyzer - 按照空格切分,不转小写
- Keyword Analyzer - 不分词,直接将输入当作输出
项目中使用IK分词器,对中文分词有良好支持
索引:类似数据库表名,就是一个拥有几分相似特征的文档的集合
映射:类似数据库表结构,档和它所包含的字段如何被存储和索引的过程
文档:文档是索引中存储的一条条数据
字符串类型: keyword 关键字 关键词 、text 一段文本
数字类型:integer long
小数类型:float double
布尔类型:boolean
日期类型:date
@PostConstruct:在项目启动的时候执行这个方法,也可以理解为在spring容器启动的时候执行,可作为一些数据的常规化加载,比如数据字典之类的。
使用:
首先通过@PostConstruct建立在项目启动时建立索引,并将帖子数据加载到es中。
实现帖子保存,删除,条件查询(高亮显示),帖子条数
帖子有添加,增加评论时,通过kafka异步提交到Elasticsearch服务器。
使用Kafka进行解耦,削峰
Kafka介绍
Kafka简介
- Kafka是一个分布式的流媒体平台。
-应用:消息系统、日志收集、用户行为追踪、流式处理。Kafka特点
–高吞吐量、消息持久化、高可靠性、高扩展性。
1)
-发布帖子时,通过kafka将帖子异步的提交到Elasticsearch服务器。
-增加评论时,通过kafka将帖子异步的提交到Elasticsearch服务器。
(在消费组件中方法:消费帖子发布事件,消费删帖事件)
2)
发布通知是分频繁的(如某人点赞,评论了帖子,关注了某用户),所以用kafka消息队列解决问题,设置三个主题,评论,点赞,关注,一旦事件发生,就可以把这件事包装成消息,放到相应队列中,提交后生产者就不用管了。
消费者取出消息将解析到Message对象中,存入数据库mesagge表中
3)通过kafka将wk生成的长图上传到七牛云中。
因为生成图片时间比较长且长传图片也需要时间,而且可能生成上传图片不成功,为了不影响主线程业务,controller只需把事件丢给kafka,后续交给kafka异步处理就行。
将上传任务交给kafka,kafka通过ThreadPoolTaskScheduler线程池上传
具体流程:
在消费者编写消费分享事件:
根据取出的消息记录,先通过wk将图片生成到本地,启用定时器定时(每隔500s)尝试执行上传图片,先从本地获取文件存放的路径,设置响应信息、生成上传凭证、指定上传机房、开始上传图片。在执行任务时,如果长时间没上传成功(可能是图片生成失败),停止线程执行任务,如果上传次数超过3次(可能七牛云有问题或网络阻塞),也停止线程不上传了。
为什么刷新帖子分数用的是分布式定时任务,而这里使用spring的定时任务,不会存在问题吗?
可以用scheduler的原因:这个逻辑并不是每个服务器都会执行,虽然每个服务器都部署了consumer,但是消费者有一个抢占的机制,比如有5台服务器,部署了5个consumer,
当消息发送后,只有一个consumer会处理,其他不处理,这是所有消息队列的机制。所以这个方法只有一个服务器执行,其他服务器不执行,在这个服务器上启动定时器和其他服务器没有关联。
个定时器和之前那个定时器不一样,之前那个服务器一启动所以服务器定时器都会执行,而这里谁抢到了这个消息,谁就执行这个定时器
Redis实现点赞,关注,缓存热点数据,实现UV,DAU,Redis事务
1)可对帖子,评论进行点赞,查询某用户获得的赞(统计该用户发布的帖子,评论被赞的总数)
点赞 | 取消点赞、统计用户点赞数
缓存某用户获得的赞数量和某实体的点赞用户id
某实体的赞:
like:entity:entityType:entityId -> set(userId)
某个用户的赞
like:user:userId -> int
2)关注 | 取消关注、统计用户的关注数和粉丝数
缓存某用户关注的实体
followee:userId:entityType -> zset(entityId,now)
followee:userId(用户):entityType(关注的实体类型,如视频,帖子,某人)->zset(entityId(关注的实体Id),now(用时间做分数))
缓存某实体拥有的粉丝
follower:entityType:entityId -> zset(userId,now)
follwer:entityType(某个实体,如视频,话题,某人):entity(实体id)->zset(用户id,时间)
事务:
点赞和被点赞实体的用户对应的点赞数量需要原子执行
A关注了B,则添加A关注的实体和B的粉丝需要原子执行
4)使用Redis统计网站UV,DAU
. HyperLogLog
-采用一种基数算法,提供不精确的去重计数方案(这个不精确并不是非常不精确),标准误差是0.81%,对于UV这种统计来说这样的误差范围是被允许的
-占据空间小,输入元素的数量或者体积非常大时,基数计算的存储空间是固定的。在Redis中,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同的基数。
但是:HyperLogLog只能统计基数的大小(也就是数据集的大小,集合的个数),他不能存储元素的本身,不能向set集合那样存储元素本身,也就是说无法返回元素。
.Bitmap
-不是一种独立的数据结构,实际上就是字符串。
-支持按位存取数据,可以将其看成是byte数组。
- 适合存储索大量的连续的数据的布尔值。
. UV (Unique Visitor)
-独立访客,需通过用户IP排重统计数据。
-每次访问都要进行统计,以对多次数据去重,使用HyperLogLog去重可以节约内存
DAU (Daily Active User)
-日活跃用户,需通过用户ID排重统计数据(DAU更关注用户的有效性,只能登录的才能统计)。
-访问过一次,则认为其活跃。
- Bitmap,性能好、且可以统计精确的结果。
5)使用Redis缓存用户信息
每次拿到请求获取凭证后都需要根据凭证查用户,所以将当前访问接口的用户缓存在redis中。获取时先尝试从缓存中读,若没有再从数据库。
缓存的信息只要数据变化就清除。如修改用户信息时(头像,用户名,密码),先更新数据库,在清除缓存
6)使用Redis存储验证码
验证码需要频繁的访问与刷新,对性能要求较高。
验证码不需永久保存,通常在很短的时间后就会失效。
分布式部署时,存在Session共享的问题,所以是使用redis缓存。
6)使用Security+JWT完成登录认证,权限校验
注册->发送邮件进行激活->登录,JWT.XXXXXXXXXXXXXXXXXXXX(在补充)
密码使用MD5加密,因为MD5每次加密结果一样,为了不被破解,所以是对用户密码+盐值加密
激活,1,已经激活过,0未激活
注册 、电子邮件激活
注册时密码需要进行加密,使用MD5对同一个数据加密结果是一样的,所以将用户密码与用户的盐值一起进行Md5加密。保证不被破解
用户的密码在前端使用了MD5加密方式,保证传输过程的安全性,然后后端使用Brcty加密存入数据库
MD5和Brcty都是单向的加密算法,加密后的密码不能逆向解密,但是可以校验他加密后的结果是否和抬头存的结果一致
MD5对于同于一个字符串每次加密都是一样的,Brcty每次执行加密都是不一样的,他由Brcty的算法版本,算法的强度,随机生成的盐,和字符串的hash值组成,它的每次加密结果都不一样,安全性更高
登录
JWT原理
JWT主要用来封装用户信息方便对客户端访问的接口进行安全认证。
首先配置文件中配置了秘钥和token头部标识,jwt的过期时间。然后将jwt涉及到的相关的常用操作封装在了一个工具类中,主要包括根据用户信息生成token,从token中获取用户信息,判断token是够否有效,刷新token啥的。用户登录时,通过用户名从数据库中加载用户真实信息,然后进行用户信息,验证码的校验,如果不匹配对应错误信息,验证成功则将用户保存在secutity的上下文中,方便下次登录时访问。
之后用户每访问一个接口都需要携带jwt,每次都会被自定义过滤器拦截,验证客户端传过来的token信息是否正确和用户是否有登录,如果未登录,则重定向到登录页面,以及校验是否可以有权限访问接口
SpringSecurity自己的用户信息只包含了Username,password,roles,假如我希望用户的实体类中还有性别sex字段,那么就没有办法了,所以SpringSecurity提供了UserDetails接口,我们可以自己新建一个包含sex字段的类,然后该类implements UserDetails接口,就可以获取我们说的这个sex字段
UserDetails实例是通过UserDetailsService接口的loadUserByUsername方法返回的,可以参考这篇文章创建自己的UserDetails(本文中包含sex的那个类)
UserDetails接口中有一个getAuthorities方法,这个方法返回的是权限,但是我们返回的权限必须带有“ROLE_”开头才可以,spring会自己截取ROLE_后边的字符串,也就是说,比如:我的权限叫ADMIN,那么,我返回告诉spring security的时候,必须告诉他权限是ROLE_ADMIN,这样spring security才会认为权限是ADMIN
安全认证和授权
安全验证主要使用security+Jwt实现的,引入security后,所有请求都被保护起来,默认使用的是WebSecurityConfigurerAdapter中的配置,我们通过继承他自定义自己的安全访问策略,里面主要重写了三个方法名为config的方法,一个是用来配置用来配置 WebSecurity,配置了哪些无须认证可以直接访问的公共资源,另一个配置 HttpSecurity 。 HttpSecurity 用于构建一个安全过滤器链 SecurityFilterChain 。SecurityFilterChain 最终被注入核心过滤器。在里面主要注入了自定义的权限控制过滤器,还有jwt登录认证过滤器和自定义未授权结果返回类等。
自定义jwt登录认证过滤器用来验证当前用户是否登陆是否合法,首先在用户登录的时候,会通过jwt生成安全令牌,安全令牌中携带了秘钥和token头部标识,和用户的信息,之后用户每访问一个接口都需要携带jwt,自定义过滤器拦截会对请求的接口进行拦截认证,如果jwt的头部标识和设置的标识一样且没有过期,jwt携带的用户信息也和security中的用户信息一样,则当认证成功,证成功后会生成一个UsernamePasswordAuthenticationToken凭证放在Authentication中,方便之后权限校验使用。
认证后会对用户访问的接口进行权限校验。
把当前请求所需的所有 ConfigAttribute 传递给 **AccessDecisionVoter** 进行决策,只要任一与用户拥有 GrantedAuthority 匹配,即代表授予访问权限。
认证后,不同用户有不同角色,他们能操作的资源也是不同的,所以不同用户可以访问哪些资源,这部分叫授权,即访问控制,控制谁能访问哪些资源,主体进行身份认证后需要分配访客访问系统的资源,对于某些资源没有权限是无法访问的。
权限控制自定义了两个过滤器,一个实现了FilterInvocationSecurityMetadataSource接口,可以用来获取当前访问的受保护的接口所需要的权限信息,权限信息保存在configAttributes中,如果没有匹配的为他设置一个默认用户role_login,默认可以访问该接口。另一个实现了AccessDecisionManager自定义访问决策管理器接口,用来校验用户访问的接口当前用户是否有权限访问,这里主要依次遍历configAttributes中的权限信息,只要任一权限与用户拥有的 GrantedAuthority(角色信息) 匹配,则有权访问,否则抛出自定义的异常返回结果
对当前系统内包含的所有的请求,分配访问权限(普通用户、版主、管理员)。
置顶,加精,删除,修改帖子状态
版主:置顶,加精
管理员可以执行删除
普通用户只能查看,评论等
CSRF配置
-防止CSRF 攻击的基本原理,以及表单、AJAX相关的配置。
//csrf攻击:某网站盗取cookie中的凭证,模拟你的身份去访问服务器,向服务器提交表单获利
//security解决方案,security会生成一个隐藏的凭证token,浏览器每次访问security都会返回一个隐藏的token,该token无法被三方识别到
使用ThreadLocal缓存当前用户信息
方便在请求线程之内随时获得用户信息,以及用于线程隔离;
在请求结束后清除用户数据
私信列表,发送私信:
数据库中定义了mesage私信数据表,发送私信时将私信内容添加到数据库中
message
字段 | 描述 |
---|---|
id | 消息id |
from_id | 发送者(为1则是系统通知的消息) |
conversation_id | 会话id(发送人id_接收人id)【如果是系统消息,这存发送人id_接收人id没意义,存消息主题】 |
content | 内容 |
status | 状态 |
create_time | 创建时间 |
** 统一异常处理 | 统一日志处理**
@ControllerAdvice对Controller类方法做全局配置,@ExceptionHandler注解对Controller方法异常进行处理,当方法出现异常时,如果是普通请求,则重定向到404页面,如果是ajax请求,返回"服务器异常"提示字样
统一日志处理
通过AOP记录server包下的所有service类方法执行日志,存到指定文件中。
使用分布式定时任务Spring Quartz定时更新帖子分数
为什么要使用SpringQuartz而不是别的线程池?
在分布式架构中,每个服务的定时任务数据都是独立不共享的,同一个任务每个都执行一遍就会有问题,而quartz的运行过程中线程相关信息(触发器,任务详细信息等)是存在数据库中,他们会通过加锁方式抢到运行的资源,某个抢到数据库中数据,他会检查数据时运行还是没有运行
PREFIX_POST + SPLIT + “score”;
log(精华分+评论数10 +点赞数2+收藏数*2〉+(发布时间-牛客纪元)
求log原因是因为刚开始变化趋势很明显(因为前期点赞,收藏,评论权重高,但后期点了100个赞与前期点10个赞效果是一样的,为了让变化趋于平滑),随着时间推移,分数逐渐降低。
流程:
做了一个操作,这个操作(添加帖子,发布评论,点赞,加精)会影响帖子分数时,将要计算分数的帖子id存到redis中,定时的对这些分数变化的帖子计算分数。(操作一下算一些效率低,比如点赞是很高频操作,一下几十个人点赞,还没计算好,又有人点赞,效率低,不合理,所以将id先缓存在统一进行计算)
使用cafinne缓存前几页数据和帖子总数
因为热门帖子不会全部访问,一般前面热面几页看得多,访问频率高
为什么使用caffine缓存而不redis缓存?
缓存热门帖子,缓存更适合存储数据变化频率比较低的数,如用户数据,但是帖子经常被点赞,评论,变化频繁,使用redis存就要经常更新缓存导致效率降低。
同时和本地缓存相比,本地缓存比redis缓存性能更好,因为请求发给应用服务器,应用服务器要访问NoSQL,往往在两台机器上,就会有网络开销。
本地缓存使用定时清理策略,看到数据不是最新的,但相对是最新的。
查询热门帖子某一页数据时,先以offset:limit为key从本地缓存中获取帖子,如果没有,则会自动根据指定的方式从数据库加载数据到缓存中。
缓存帖子,key是offset:limit,这两参数确定一页。
边栏推荐
- 编写 BOLL 心得体会
- 【项目实现】Boost搜索引擎
- Dong mingzhu live cold face away, when employees frequency low-level mistakes, no one can understand their products
- 2022焊工(初级)上岗证题目模拟考试平台操作
- 各位大佬好,麻烦问一下flink cdc oracle写入doris的时候,发现cpu异常,一下下跑
- 如何在MySQL中的数据库下删除所有的表
- Good bosses, please ask the flink CDC oracle to Doris, found that the CPU is unusual, a run down
- 【翻译】Terraform和Kubernetes的交集
- sqoop ETL工具
- 织梦内核电动伸缩门卷闸门门业公司网站模板 带手机版【站长亲测】
猜你喜欢
DIY电工维修如何拆卸和安装开关面板插座
In a more general sense, calculating the displacement distance and assumptions
Why use Selenium for automated testing
参加Oracle OCP和MySQL OCP考试的学员怎样在VUE预约考试
共n级台阶,每次可以上1级或2级台阶,有多少种上法?
说说数据治理中常见的20个问题
【Playwright测试教程】5分钟上手
LeetCode每日一题(2285. Maximum Total Importance of Roads)
瑞能微计量芯片RN2026的实用程序
There are too many systems, how to realize multi-account interworking?
随机推荐
STM8S105K4T6------串口发送和接收
STM8S project creation (STVD creation) --- use COSMIC to create a C language project
[Playwright Test Tutorial] 5 minutes to get started
Dong mingzhu live cold face away, when employees frequency low-level mistakes, no one can understand their products
Mockito单元测试
【医保科普】维护医保基金安全,我们可以这样做
Deep Learning (3) Classification Theory Part
【Playwright测试教程】5分钟上手
单片机C语言->的用法,和意思
MallBook 助力SKT思珂特教育集团,立足变化,拥抱敏捷交易
P3384 【模板】轻重链剖分/树链剖分
查看mysql死锁语法
APP电商如何快速分润分账?
织梦响应式酒店民宿住宿类网站织梦模板(自适应手机端)
Homemade bluetooth mobile app to control stm8/stm32/C51 onboard LED
三分建设,七分管理!产品、系统、组织三管齐下节能降耗
香港服务器有哪些常用的型号
Architecture of the actual combat camp module three operations
为什么用Selenium做自动化测试
2022年最新海南建筑八大员(材料员)模拟考试试题及答案