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队列,因为上面已经一些新鲜事了。