牛客网高级项目总结

时间:2024-03-18 19:24:05

牛客网高级项目总结
1、注册和登陆

登陆和注册成功之后,在cookie里添加上token,另外在数据库中插入包含token、userId的表,用于登陆状态检验。
具体检验是在拦截器上进行,拦截器的实现过程:1)继承HandlerInterceptor接口,完成preHandle()用于请求之前、postHandle 渲染界面之前、afterCompletion 请求之后,三个方法。 2) 之后把得到的拦截器对象在 实现了WebMvcConfigurerAdapter的对象上进行注册。registry.addInterceptor(passportInterceptor);

2、未登录状态下的请求拦截器
preHandle用于判断某个请求的前提是已登陆状态。如果没有登陆,那么跳转到/login?next= + httpServletRequest.getRequestURI() ,登陆界面获取到这种类型的url,再实现登录状态后的跳转。

3、多线程下保存每条线程的静态变量
使用ThreadLocal

private static ThreadLocal<User> users = new ThreadLocal<>();
1
4、敏感词过滤

前缀树实现的一个匹配

package com.nowcoder.service;

import org.apache.commons.lang.CharUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

public class SensitiveService implements InitializingBean {

    private static final Logger logger = LoggerFactory.getLogger(SensitiveService.class);

    /**
     * 默认敏感词替换符
     */
    private static final String DEFAULT_REPLACEMENT = "****";

    /**
     * 继承了InitializingBean
     * 在第一次调用时,先执行下面的方法
     * 拿来做敏感词导入
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        rootNode = new TrieNode();
        try {
            InputStream is = Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream("SensitiveWords.txt");
            InputStreamReader reader = new InputStreamReader(is);
            BufferedReader bufferedReader = new BufferedReader(reader);
            String lineTxt;
            while ((lineTxt = bufferedReader.readLine()) != null) {
                lineTxt = lineTxt.trim();
                addWord(lineTxt);
            }
            reader.close();
        } catch (Exception e) {
            logger.error("读取敏感词文件失败 "+ e.getMessage()) ;
        }
    }

    /**
     * 前缀树的数据结构,每个节点保存的数据:
     * 1)end标志 2)下一层节点的节点集,map<Character, TrieNode >,通过char进行判断是否挂在下面
     */
    private class TrieNode{

        private Map<Character, TrieNode> subNodes = new HashMap<>();
        private boolean end = false;

        /**
         * 向当前的TrieNode节点后挂上节点树
         * @param key
         * @param trieNode
         */
        void addSubNode(Character key, TrieNode trieNode) {
            subNodes.put(key, trieNode);
        }

        /**
         * 通过key在下一层节点的map中进行查询返回
         * @param key
         * @return
         */
        TrieNode getSubNode(Character key) {
            return subNodes.get(key);
        }

        boolean isKeywordEnd() {
            return end;
        }

        void setKeyWordEnd(boolean end) {
            this.end = end;
        }

        public int getSubNodeCount() {
            return subNodes.size();
        }
    }

    /**
     * 根节点,空
     */
    private TrieNode rootNode = new TrieNode();

    /**
     * 判断字符是否合法
     * @param c
     * @return
     */
    private boolean isSymbol(char c) {
        int ic = (int)c;
        // 英文 和 东亚文字范围
        return !CharUtils.isAsciiAlphanumeric(c) && (ic < 0x2E80 || ic > 0x9FFF);
    }

    /**
     * 添加字符串到前缀树
     * @param lineTxt
     */
    private void addWord(String lineTxt) {
        TrieNode tempNode = rootNode;
        for (int i = 0; i < lineTxt.length(); i++) {
            Character c = lineTxt.charAt(i);
            if (isSymbol(c)) {
                continue;
            }
            // 通过字符c 查找是否在前缀数上有分支
            TrieNode node = tempNode.getSubNode(c);

            if (node == null) {
                //不存在当前字符
                node = new TrieNode();
                tempNode.addSubNode(c,node);
            }
            tempNode = node;
            if (i == lineTxt.length() - 1) {
                tempNode.setKeyWordEnd(true);
            }
        }
    }

    /**
     * 过滤敏感词
     * @param txt
     * @return
     */
    public String filter(String txt) {

        if (StringUtils.isBlank(txt)) {
            return txt;
        }
        String replacement = DEFAULT_REPLACEMENT;
        StringBuilder result = new StringBuilder();

        //三个指针
        TrieNode tempNode = rootNode;
        int begin = 0;// 回滚的位置
        int position = 0; // 当前比较的位置

        while (position < txt.length()) {
            char c = txt.charAt(position);
            // 遇到特殊字符
            if (isSymbol(c)) {
                if (tempNode == rootNode) {
                    result.append(c);
                    ++begin;
                }
                ++position;
                continue;
            }
            // 拿c字符去询问,是否在前缀树上
            tempNode = tempNode.getSubNode(c);
            if (tempNode == null) {
                //以begin开始的字符串不存在敏感字符
                result.append(txt.charAt(begin));

                position = begin + 1;//移到下一位
                begin++;
                tempNode = rootNode;//恢复树根
            } else if (tempNode.isKeywordEnd()) {
                // 发现了敏感词
                result.append(replacement);
                position = position + 1;//直接移到敏感词后面
                begin = position;
                tempNode = rootNode;
            }else
                ++ position;
        }
        // 后面没处理完的字符串
        result.append(txt.substring(begin));
        return result.toString();
    }

    public static void main(String[] args) {
//        System.out.println("string:哈哈哈哈");
        SensitiveService sensitiveService = new SensitiveService();
        sensitiveService.addWord("fuck");
        sensitiveService.addWord("bich");
        System.out.println(sensitiveService.filter("hi bich  fu ck"));

    }
}


5、使用了Redis实现了点赞点踩功能

使用Redis的集合,一个实体例如Question,都是由entityType和entityId标识的,我们通过这两个参数去标识一个key,例如 LIKE:1:2 。我们通过这个key去建立一个set,里面保存userId,假设用户喜欢一个问题,就在问题的set里面添加上这个userId。

PV  //讨论区里的浏览数在Redis中存储
点赞 //点赞,userId放在Redis中
关注 //人与人之间关注,关注者是个集合,放进Redis,如果取消关注,从集合中删除即可
排行榜 //登陆数,登陆一次,数值加1
验证码 //验证码,用到set timeout时间,后台生成验证码,放进Redis,设置过期时间,比如三分钟还没收到用户验证码,自动从Redis删除验证码。
缓存 //牛客网*问,打开网页用户头像,用户名昵称,如果用户不更新,用户信息等在MySQL上可以做一层缓存,如果用户没更新信息,可以直接从缓存读,如果缓存没有,直接去数据库取,大大降低数据库的压力。
异步队列 //点了赞,谁评论了帖子,发生事件,放进redis,后面有线程去执行
判题队列  //用户提交判题,放入判题队列,在Redis中,服务器连判题队列执行,如果碰到高峰期,可以加机器 

在高并发的情况下,比如秒杀,很多用户访问网页,评论网页,我们要记录商品数,浏览量,评论数,如果每一次请求后都去数据库中update字段,服务器很容易卡死。因为在更新数据库的时候,对该字段的行是上锁的。解决的办法是将数组存到某一个地方,每次收到请求后服务器先记录,每隔一段时间写回数据库中,这样用户看到的是一秒前的访问量,过了一秒,访问量蹭的上去了,对于用户来说一秒反应是没什么感觉的,对于服务器能防止卡死。可以将值存到Redis中。

Redis可以设置有效期,在登陆的时候可以用到,把用户登陆服务器下发的token存到Redis中,设置过期时间,时间到自动删除了。比存在数据库中判断expired_time更方便。

6、异步队列

具体看这里吧

7、关注业务

牛客网高级项目总结
A关注了B,那么A关注的人(followee)就需要添加一组数据, B谁关注了我(粉丝,follower)。
这里有多组操作,需要体现事务性。

8、timeline推拉

例如:用户点赞/收藏了一条评论,或者发送一个动态就会产生一个新鲜事,保存在mysql数据库中

Feed的字段:

int id 标识
type 新鲜事的类型
userId 由哪个用户引发的新鲜事
createdDate
data 以Json格式存储的新鲜事内容
如何推拉
每个人都有一个自己的redis队列,key是用户id, value是json格式的信息。

牛客网高级项目总结

实现推:把本人的新鲜事,推送给粉丝。因此传递本人的id,在数据库中查询出自己的feed行,再对每个用户的redis队列进行add。

实现拉:直接调用自己的redis队列,因为上面已经一些新鲜事了。