基于SpringBoot的SSM宅码论坛项目总结与分享(二)

3 篇文章 0 订阅
3 篇文章 0 订阅

5.发布帖子与敏感词过滤

使用AJAX异步发帖

  • AJAX - Asychronous JavaScript and XML
    
  • 异步的JavaScript与XML, 不是一门新的技术,只是一门新的术语
    
  • 使用AJAX,网页能够将改变的量更新呈现在页面上,而不需要刷新整个页面
    
  • 虽然X代表XML,但是目前JSON的使用比XML更加普遍
    

发布帖子的时候需要对帖子的标题和内容进行敏感词,通过Trie实现敏感词过滤算法,过滤敏感词首先需要建立一棵字典树,并且读取一份保存敏感词的文本文件,并用文件初始化字典树,最后将敏感词作为一个服务,让需要过滤敏感词的服务进行调用即可。字典树数据结构的实现。
在这里插入图片描述


@Component
public class SensitiveFilter {

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

    // 替换符
    private static final String REPLACEMENT = "***";

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

    @PostConstruct
    public void init() {
        try (
                InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
                BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        ) {
            String keyword;
            while ((keyword = reader.readLine()) != null) {
                // 添加到前缀树
                this.addKeyword(keyword);
            }
        } catch (IOException e) {
            logger.error("加载敏感词文件失败: " + e.getMessage());
        }
    }

    // 将一个敏感词添加到前缀树中
    private void addKeyword(String keyword) {
        TrieNode tempNode = rootNode;
        for (int i = 0; i < keyword.length(); i++) {
            char c = keyword.charAt(i);
            TrieNode subNode = tempNode.getSubNode(c);

            if (subNode == null) {
                // 初始化子节点
                subNode = new TrieNode();
                tempNode.addSubNode(c, subNode);
            }

            // 指向子节点,进入下一轮循环
            tempNode = subNode;

            // 设置结束标识
            if (i == keyword.length() - 1) {
                tempNode.setKeywordEnd(true);
            }
        }
    }

    /**
     * 过滤敏感词
     *
     * @param text 待过滤的文本
     * @return 过滤后的文本
     */
    public String filter(String text) {
        if (StringUtils.isBlank(text)) {
            return null;
        }

        // 指针1
        TrieNode tempNode = rootNode;
        // 指针2
        int begin = 0;
        // 指针3
        int position = 0;
        // 结果
        StringBuilder sb = new StringBuilder();

        while (position < text.length()) {
            char c = text.charAt(position);

            // 跳过符号
            if (isSymbol(c)) {
                // 若指针1处于根节点,将此符号计入结果,让指针2向下走一步
                if (tempNode == rootNode) {
                    sb.append(c);
                    begin++;
                }
                // 无论符号在开头或中间,指针3都向下走一步
                position++;
                continue;
            }

            // 检查下级节点
            tempNode = tempNode.getSubNode(c);
            if (tempNode == null) {
                // 以begin开头的字符串不是敏感词
                sb.append(text.charAt(begin));
                // 进入下一个位置
                position = ++begin;
                // 重新指向根节点
                tempNode = rootNode;
            } else if (tempNode.isKeywordEnd()) {
                // 发现敏感词,将begin~position字符串替换掉
                sb.append(REPLACEMENT);
                // 进入下一个位置
                begin = ++position;
                // 重新指向根节点
                tempNode = rootNode;
            } else {
                // 检查下一个字符
                position++;
            }
        }

        // 将最后一批字符计入结果
        sb.append(text.substring(begin));

        return sb.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);
        }

    }

}

防止xss注入:防止xss注入直接使用HTMLUtils的方法即可实现。
在普通文本的基础上加入了Markdown,使得发表文章可以使用富文本形式。

<div id="md-content" style=" z-index: 1 !important;">
            <textarea placeholder="博客内容" name="content" id="message-text"  style="display:none;" th:text="*{content}">
       		 </textarea>
 </div>

并加上富文本编辑器:

<script>
    // 初始化MarkDown编辑器
    var contentEditor;
    $(function() {
        contentEditor = editormd("md-content", {
            width   : "100%",
            height  : 480,
            syncScrolling : "single",
            path    : "/community/lib/editormd/lib/", /*因为application.yml中配置了项目根路径为/blog,所以要加上才能访问*/
            toolbar : true,
            watch   : false,
            imageUpload : true,
            imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
            imageUploadURL : "{:url('/upload')}"

        });
    });
</script>

实现效果图:

在这里插入图片描述

6.Kafka,实现异步消息系统,私信与系统通知的实现

在项目中,会有一些不需要实时执行但是是非常频繁的操作或者任务,为了提升网站的性能,可以使用异步消息的形式进行发送,再次消息队列服务器kafka来实现。
在这里插入图片描述

发送系统通知

评论,点赞,关注等事件是非常频繁的操作,发送关系其的系统通知却并不是需要立刻执行的。主要实现分为下面几步:
• 触发事件

  1. 评论后,发布通知
  2. 点赞后,发布通知
  3. 关注后,发布通知

• 处理事件

1. 封装事件对象(Event)

 	private String topic;
    private int userId;
    private int entityType;
    private int entityId;
    private int entityUserId;
    private Map<String, Object> data = new HashMap<>();

2. 开发事件的生产者
向特定的主题(评论,点赞,关注)发送事件

@Component
public class EventProducer {

    @Autowired
    private KafkaTemplate kafkaTemplate;

    // 处理事件
    public void fireEvent(Event event) {
        // 将事件发布到指定的主题
        kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
    }

}

3. 开发事件的消费者
使用@KafkaListener注解监听事件,如果监听成果并进行相应的处理,最后调用messageService添加到数据库中,下次用户显示消息列表的时候就可以看到系统消息了。根据消费者所订阅的主题,进行消费。@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
具体操作可以参照:
Kafka系列第三篇!10 分钟学会如何在 Spring Boot 程序中使用 Kafka 作为消息队列?
在这里插入图片描述

效果图:
在这里插入图片描述
在这里插入图片描述

7.私信与系统通知的实现

私信和系统通知公用一张表:

• 私信通知表:

idform_idto_idconversion_idcontentstatuscreate_time
	○ form_id : 发送者,其中from_id=1表示系统通知;
	○ To_id :收信人;
	○ Coversion_id:111_112(id小的在前)表示111与112之间的会话。如系统通知则会存具体类型like,comment,follow;
	○ status:0未读,1已读,2删除;

通知私信详情页面:在这里插入图片描述

8.总结

本项目基于牛客高级项目课,其余的可以参照源码进行解读,源码地址:
https://github.com/Wenbin94/community-forum-By-SpringBoot
在这里插入图片描述

9.面试总结

一般会从以下部分进行考察:
在这里插入图片描述
MySQL
1.数据库的字段是怎么设计的?

• 评论:id	user_id	entity_type	entity_id	   target_id	  content	  status	 create_time
			○ user_id:评论作者;
			○ entity_type:评论目标类型:1表示帖子,2表示评论 支持回复评论;
			○ target_id 记录回复指向的人 (只会发生在回复中 判断target_id==0)
			○ user_id 评论的作者
			○ status:0存在,1表删除
		
• 私信:id	 form_id	  to_id	conversion_id	content	status	create_time
		○ form_id : 发送者,其中from_id=1表示系统通知
		○ To_id :收信人。
		○ Coversion_id:111_112(id小的在前)111与112 之间的会话。
		○ status:0未读,1已读,2删除
• 帖子:id, user_id, title, content, type, status, create_time, comment_count, score
• 用户user:id, username, password, salt, email, type, status, activation_code, header_url, create_time

2.帖子回复里的多级评论如何实现?

通过comment字段中entity_type分辨评论的目标类型,所以可以在回复帖子列表中只会展示entity_type==1的回复帖子的分页,
而回复的回复entity_type==2并根据记录回复target_id可以判断是否回复回复的人并显示。

3.存储引擎,MyISM和InnoDB的区别?

在这里插入图片描述
4.索引模块?

MySQL索引使用的数据结构主要有BTree索引 和 哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候
,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。MySQL的BTree索引使用的是B树中的B+Tree,但对于主要的两种存储引擎
的实现方式是不同的。

• MyISAM: B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出
其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
• InnoDB: 其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节
点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。而其余
的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。在根据主索引搜索时,直接找到key所在的
节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不
建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。 

5.既然你提到InnoDB使用的B+ Tree的索引模型,那么你知道为什么采用B+ 树吗?这和Hash索引比较起来有什么优缺点吗?

因为Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是
无法直接通过索引查询的,就需要全表扫描。所以,哈希索引只适用于等值查询的场景。而B+ Tree是一种多路平衡查询树,所以他的节点是天然有序的(
左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描。

6.为什么要给表建立主键?

如果给表上了主键,那么表在磁盘上的存储结构就由整齐排列的结构转变成了树状结构,也就是上面说的「平衡树」结构,换句话说,就是整个表就变成了
一个索引。没错, 再说一遍, 整个表变成了一个索引,也就是所谓的「聚集索引」。 这就是为什么一个表只能有一个主键, 一个表只能有一个「聚集索引
」,因为主键的作用就是把「表」的数据格式转换成「索引(平衡树)」的格式放置。

7.索引有什么优缺点?

	索引能让数据库查询数据的速度上升, 而使写入数据的速度下降,原因很简单的, 因为平衡树这个结构必须一直维持在一个正确的状态, 增删改数据
	都会改变平衡树各节点中的索引数据内容,破坏树结构, 因此,在每次数据改变时, DBMS必须去重新梳理树(索引)的结构以确保它的正确,这会
	带来不小的性能开销,也就是为什么索引会给查询以外的操作带来副作用的原因。

8.说说非聚集索引(常规索引)?
在这里插入图片描述
9.并发事务带来哪些问题?

• 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然
后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
• 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也
修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改
A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
• 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中
的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况
,因此称为不可重复读。
• 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随
后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复读和幻读区别:
不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减
少了

10.事务隔离级别有哪些?MySQL的默认隔离级别是?
在这里插入图片描述
11.什么是SQL注入,如何避免SQL注入

SQL注入其实就是恶意用户通过在表单中填写包含SQL关键字的数据来使数据库执行非常规代码的过程。使用数据库预编译,而SQL注入只对编译过程有破坏
作用,执行阶段只是把输入串作为数据处理,不需要再对SQL语句进行解析,因此解决了注入问题。在mybatis里使用#{}就会使用预编译功能,并将参数替
换为占位符“?”。而${}不回采用预编译功能可能产生sql注入风险。

12.主从分离,读写分离说一说?
在这里插入图片描述
13.联合索引和最左匹配
在这里插入图片描述
Redis和Spring的问题总结将在下个帖子。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值