项目源代码已托管在 Github,欢迎 Star、Fork。
QA 是一个基于 B/S 架构而设计开发的社区网站。html
主要为用户提供如下服务:前端
Spring Boot + MyBatis + MySQL + Redis + FreeMarkerjava
为了保证用户信息安全,系统对用户密码采用「salt + md5」方式进行加密。用户注册/登陆成功后,系统会生成一个 ticket ,将 ticket 与用户 id 相关联,并将此信息插入到数据库表 login_ticket 中,同时将 ticket 响应给客户端。node
用户每次请求页面的时候,都须要先通过 PassportInterceptor 拦截器,拦截器判断此 ticket 是否真实有效,如果,根据 ticket 对应的用户 id ,查出相应用户信息,并添加至页面上下文中。git
在以上 UGC (User Generated Content, 用户产生的内容)中,系统都会进行 HTML 标签及敏感词过滤,这在必定程度上防止网站被注入脚本或者充斥着不良信息。github
若没有对 HTML 标签进行处理,当用户发布的内容含有如
<script>alert("hahah");</script>
时,网站页面每次加载此内容时都会弹出消息框。
对于敏感词过滤,按照常规的思惟,也是最简单的方式,就是:对于每一个敏感词,都在文本中查找该敏感词是否出现,出现则进行替换。这种方式,每一个敏感词都要在一段文本中进行遍历查找,复杂度很是高。redis
本项目采用「前缀树」方式实现敏感词过滤,空间换时间,效率较高。前缀树结点结构以下:算法
class TrieNode { // 标记是否为敏感词结尾 boolean end; // 该结点的全部直接子结点 Map<Character, TrieNode> subNodes = new HashMap<>(); // 添加一个子结点 void addSubNode(Character key, TrieNode node) { subNodes.put(key, node); } // 根据key获取子结点 TrieNode getSubNode(Character key) { return subNodes.get(key); } }
后台从敏感词文件 SensitiveWords.txt 顺序读取每一行创建前缀树。进行过滤时,遍历须要过滤的文本,用星号替换发现的敏感词。假设文本长度为 len,前缀树的最大高度为 h,那么此算法的最坏时间复杂度为 O(len*h)。spring
算法比较
假如敏感词平均长度为10,数量为100000,文本长度为 len。
常规方式,复杂度O(100000 (len + 10));前缀树算法复杂度O(10 len)。
对于评论功能,系统创建的是一个统一的评论服务中心,经过 EntityType 与 EntityId 识别所评论的实体。用户对于问题/评论的回复,均可以应用此服务。查询某实体下的评论时,一样根据 EntityType 和 EntityId 查询便可。数据库
赞踩功能采用「Redis」做为数据存储。Why Redis?
比较一下 Redis 和 MySQL:
Redis 适合放一些频繁使用、比较热的数据。由于数据放在了内存中,读写性能卓越。
Redis 类型 | 数据结构 | 应用场景 |
---|---|---|
List | 双向列表 | 最新列表、关注列表 |
Set | 无序集合 | 赞踩、抽奖、已读、共同好友 |
SortedSet | 优先队列 | 排行榜 |
Hash | 哈希表 | 不定长属性数 |
KV | 单一数值 | 验证码、PV、缓存 |
除了用户内容赞踩,在本项目中,Redis 还应用于如下场景:
本小节讨论用户内容赞踩服务。
用户对某一实体点赞,会将"LIKE:ENTITY_TYPE:ENTITY_ID"做为 key ,用户 id 做为 value ,存入 like 集合中。同时移除 unlike 集合中该 key 对应的用户 id。点踩服务反之。
最后将点赞数响应给页面。
本项目涉及到多种异步事件的处理。如:
这些动做并非单一的,它们会触发一些后续的操做:
用户评论了某个问题,系统除了处理“评论”这个动做外,还须要给该问题对应的用户发送一条消息,通知说“xx评论你的问题”,或者还须要给用户增长积分/经验...
事件触发者并不关心这些后续的任务,系统处理完某个动做后就能够将结果返回给触发者,然后续的任务交给系统进行异步处理便可。
所以,设计一个异步事件处理框架尤其重要。
本项目的异步框架以下图所示:
业务触发一个异步事件,EventProducer 将该事件(EventModel)序列化并存入队列(Redis List)中,EventConsumer 开启线程循环从队列中取出事件,识别该事件的类型,找出该类型对应的一系列 EventHandler,交由这些 Handler 去处理。
EventModel 的设计以下:
class EventModel { // 事件类型 EventType type; // 事件触发者 int actorId; // 事件对应的实体 int entityType; int entityId; // 事件对应的实体的Owner int entityOwnerId; // 一些扩展字段 Map<String, String> exts; }
与评论功能相似,对于关注功能,系统一样创建了一个统一的关注服务中心,用户能够关注不一样的实体(问题/用户),只须要经过 EntityType 和 EntityId 识别便可。
在数据存储方面,采用 Redis 的 zset 完成,缘由有如下几个:
一个用户,系统存储两个集合:
①保存用户关注的实体;②保存关注用户的人。
即 A 是 B 的粉丝,B 是 A 的关注对象。 [参考资料 ]
用户关注了一个问题,须要发生两个动做:
这两个动做必须同时发生,所以,这里用到了 Redis 事务保证原子性和数据的一致性。
另外,对于关注功能,如前面所说,会触发异步事件,将消息通知被关注的实体 / 实体 Owner。
本系统未采用排名算法。若要了解相关算法,能够参考以下资料:
当用户更新动态时,该用户全部粉丝均可以在必定时间内收到新的动态(也称为新鲜事、feed),能够由 “推拉模式” 实现。
模式 | 定义 | 优缺点 |
---|---|---|
推 | 事件触发后广播给全部粉丝。 | 对于粉丝数过多的事件,后台压力较大,浪费存储空间; 流程清晰,开发难度低,关注新用户须要同步更新 feed 流。 |
拉 | 登陆打开页面时,根据关注的实体动态生成 Timeline 内容。 | 读取压力大,存储占用小,缓存最新读取的 feed,根据时间分区拉取。 |
推拉 | 活跃/在线用户推,其余用户拉。 | 下降存储空间,又知足大部分用户的读取需求。 |
具体来讲,推模式就是:事件触发后产生 feed,触发事件的用户下全部粉丝的 Timeline(redis list 实现)中都存入该 feed 的 id。而拉模式,就是当前用户去拉取本身关注的人的 feed。
更多推拉模式相关,能够参考 微博 feed 系统推拉模式。
因为系统初始数据较少,为了丰富网站内容,本项目采用 pyspider 实现对 V2EX 网站的数据爬取,存储到后台数据库,并展现在前端页面上。
安装 pyspider:
pip install pyspider
启动 pyspider:
pyspider
本项目在全文搜索服务上采用 Solr 框架,中文分词采用 Solr 自带的中文分词插件 solr_cnAnalyzer 。