爱收集资源网

免费领取qq空间赞网站,点赞关注网站

网络整理 2023-09-09 06:01

community介绍

这是一款基于Springboot的牛客峰会项目,项目实现功能:包括注册,登陆,贴子,多级评论,点赞,关注,私信,搜索,系统通知,网站数据统计,热帖排名,敏感词过滤,全局异常处理,统一日志记录等。使用技术栈:SpringBoot,Springmvc,Mybatis,SpringEmail点赞关注网站,SpringSecurity,Mysql,redis,ElasticSearch,Kafka,Quartz

疗效图

软件构架

软件构架说明

安装教程xxxxxxxxxxxx使用说明xxxxxxxxxxxx参与贡献Fork本库房新建Feat_xxx分支递交代码新建PullRequest特技使用Readme_XXX.md来支持不同的语言,比如Readme_en.md,Readme_zh.mdGitee官方博客blog.gitee.com你可以这个地址来了解Gitee上的优秀开源项目GVP全称是Gitee最有价值开源项目,是综合评定出的优秀开源项目Gitee官方提供的使用指南Gitee封面人物是一档拿来展示Gitee会员风采的栏目

UserUserMapperuser-mapper.xmlMapperTestsUserServiceDiscussPostDiscussPostMapperdiscusspost-mapper.xmlMapperTestsDiscussPostService

发送邮箱要启用顾客端SMTP服务,导出jar包,配置邮箱参数,使用JavaMailSender发送短信,使用Thymeleaf发送HTML电邮MailClientMailTestsdemo.html

注册功能:递交注册数据(通过表单递交数据;服务端验证数据是否存在,邮箱是否已注册;服务端发送激活短信),激活注册帐号(点击电邮链接,访问服务端的激活服务)register.htmlCommunityUtil(生成随机字符串,MD5加密)UserService写register()方式,实现密码加密,生成激活码,获取随机头像,激活电邮activation.htmlLoginController写register()方式,CommunityConstant常量插口让UserService实现这个插口写activation()方式,让LoginController实现这个插口写activation()方式,生成验证码:KaptchaConfig配置类LoginController写getKaptcha()方式

登陆功能:验证帐号,密码,验证码;成功时,生成登陆账簿,领取给顾客端;失败时,跳转回登陆页LoginTicketLoginTicketMapper(这儿使用注解实现dao(代码简单时可用),里面都是使用xml文件)MapperTestsUserService写login()方式LoginController写login()方式在CommunityConstant常量插口中加相应的常量login.html

退出登入时将登陆账簿改为失效状态UserService写logout()方式LoginController写logout()方式显示登录信息:拦截器:在恳求开始时查询登陆用户,在本次恳请中持有用户数据,在模板视图上显示用户数据,在恳求结束时清除用户数据AlphaInterceptor拦截器WebMvcConfig拦截器配置文件UserService写findLoginTicket()方式HostHolder持有用户信息,用于替代session对象LoginTicketInterceptor拦截器

帐号设置:上传文件setting.html配置资源上传路径UserService写updateHeader()方式UserController写uploadHeader()方式和getHeader()方式检测登陆状态使用拦截器在方式前标明自定义注解,拦截所有恳求,只处理带有该注解的方式。在用户未登入时无论怎样也不能访问到一些功能LoginRequired自定义注解(拦截器拦截的注解)LoginRequiredInterceptor拦截器WebMvcConfig拦截器配置文件

邮箱注册

对前台传来的注册表单数据进行判重判空和数据库匹配后,假如注册成功,将用户信息存入数据库:注意:密码的储存是经过米醋和md5加密的,避免密码泄漏。数据库储存了该用户的盐和加密的密码。之后发送激活电邮,用户点击链接则激活帐号。mailClient工具类封装了JavaMailSender进行邮箱激活,须要在配置文件中进行邮箱SMTP服务的配置。这儿用子线程去发送短信,避免卡顿时间过长。邮箱里的激活链接即是通过"/activation/{userId}/{code}"访问路径更改用户的status数组,使其可用。

**

登陆**

登陆时检测信息正确性的逻辑和注册时基本一致,须要对帐号进行非空、存在和激活的判定,对验证码进行判定,对密码进行非空和正确的判定,以及是否有rememberMe。登录成功后生成LoginTicket存入数据库,记录了用户ID、ticket、过期时间等,ticket数组会被装入cookie中。LoginTicket登陆账簿之所以存入数据库,是考虑到了session在分布式环境下恳求分发造成的会话状态未能保持的问题。**验证码**使用google提供的Kaptcha实现验证码,登陆时要检测验证码,逻辑如下:生成图片时存入session(前面用Redis优化):登陆时从session中取值和表单值进行比对即可

状态保持登入后须要进行状态保持,可以用上面提及的登陆账簿+Interceptor+ThreadLocal实现。拦截器在preHandle时检测cookies中是否有有效ticket,有的话就在当前恳求中持有用户信息。HostHolder类封装了ThreadLocal,ThreadLocal的目的:Tomcat服务器会使用独立线程去处理每位恳求,因而须要隔离多恳求多用户,避免信息混乱。拦截器在postHandle时若发觉该次恳请中有用户信息,须要在modelAndView中添加用户信息以保持状态。在afterCompletion清空信息即可。

附:登陆模块逻辑:步入登陆界面,随机生成一个字符串来标示这个即将登陆的用户,将这个字符串短暂的存入Cookie(60秒);动态生成验证码,并将验证码及标示该用户的字符串短暂存入Redis(60秒);为登陆成功(验证用户名、密码、验证码)的用户,随机生成登陆账簿且设置状态为有效,并将登陆账簿及其状态等信息永久存入Redis,再在Cookie中存一份登陆账簿;使用拦截器在所有的恳求执行之前,从Cookie中获取登陆账簿,只要Redis中该账簿有效并在有效期内,本次恳请都会仍然持有该用户信息(使用ThreadLocal持有用户信息点赞关注网站,保证多台服务器上用户的登陆状态同步);勾选记住我,则延长Cookie中登陆账簿的有效时间;用户登载,将账簿状态设为无效,并更新Redis中该登陆账簿的相关信息

设置头像

File文件上传,更改用户的头像链接使其可以通过url访问头像图片。图片储存位置可暂存本地,后改为云服务器。更改头像(异步恳求)将用户选择的头像图片文件上传至七牛云服务器更改密码

简单的权限管理设置页面和更改头像恳求其实必须登入能够使用,可以通过注解进行简单的权限管理:自定义@LoginRequired注解类,并添加到须要权限的方式上,之后通过拦截器进行判断。在访问当前方式时若有该注解则必须是已登陆状态

分页显示所有的贴子

支持根据“发帖时间”显示支持根据“热度排名”显示(SpringQuartz)将热帖列表和所有贴子的总量存入本地缓存Caffeine(借助分布式定时任务SpringQuartz每隔一段时间就刷新估算贴子的热度/分数—见下文,而Caffeine里的数据更新不用我们操劳,它天生都会手动的更新它拥有的数据,给它一个初始化方式就完事儿)

分页通过Page类封装分页逻辑:前台传来的current等参数通过controller的Page类参数进行封装,因而实现页面跳转。该模块可用于其他地方大量复用。

发布贴子(异步恳求)

发布贴子(过滤敏感词),将其存入MySQL异步发送恳求(使用ajax,网页才能将增量更新呈现在页面上,而不须要刷新整个页面),以通过提示框展示提示信息,回帖后台就是普通的crud。

敏感词过滤

基于Trie前缀树结构,查找效率高(消耗显存大),常应用于字符串检索,词频统计,字符串排序等。该模块可用于贴子、评论、私信等。敏感词过滤器:定义前缀树依据敏感词,初始化前缀树编撰过滤敏感词的方式

显示评论

评论部份后端的名称显示有些缺陷,有兴趣的男子伴欢迎提PR解决~关于评论模块须要注意的就是评论表的设计,掌握其中数组的含意,能够透彻了解这个功能的逻辑。评论Comment的目标类型(贴子,评论)entityType和entityId以及对那个用户进行评论/回复targetId是由后端传递给DiscussPostController的

一个贴子的详情页须要封装的信息大约如下:

添加评论(事务管理)

发布对贴子的评论(过滤敏感词),将其存入MySQL评论分为对贴子的评论(简称评论)和对评论的评论(简称回复)。entityType+entityId指示评论的对象(是贴子还是评论,之后具体Id为多少),当entityType为评论,且该条评论为回复时有效,为0表示该条回复评论的是对贴子的评论,非0则是代表该条回复评论的是回复,另外注意,贴子entity中有个关于评论的冗余数据,因而有新评论形成时须要通过事务进行更新:

私信列表

其中conversationId由fromId和toId拼接而成,小Id在前,如111_112,表示111和112之间的私信。status记录私信是否已读,当用户步入私信详情页面时,会更新未读私信状态为已读。

发送私信(异步恳求)

采用异步的形式发送私信,发送成功后手动刷新私信列表查看私信详情时,将显示的私信设置成已读状态

统一异常处理

JavaWeb的思想是异常尽量不处理,而是往下层抛给controller去处理。spring对此提供了简单支持,若有4xx或5xx异常,则返回的页面为/error包下对应的4xx.html或5xx.html。但我们有如下需求:记录错误日志对于异步/非异步提供友好提示因而我们可以用@ControllerAdvice注解实现异常处理

统一记录日志

我们须要晓得什么用户哪些时侯对哪些方式进行了访问,因而最好能提供日志记录,一个解决思路是对于每位方式都进行硬编码记录日志,但这些思路似乎违反了开闭原则,不利于扩充和维护。因而我们可以用AOP的思想解决问题,Spring对此提供了友好支持:(AOP面向切面编程,可以进一步提升编程效率)切入时机可以是pointcut调用前(@Before),pointcut调用后(@After),环绕pointcut(@Around),return后(@AfterReturning),异常后(@AfterThrowing)。这儿只需@Before即可。

点赞关注

点赞关注使用较频繁,是通过Redis实现的。我们在配置类上将RedisTemplate注入Spring容器,该Bean须要设置key和value的序列化方法(存入的有可能是对象,采用json格式进行序列化)。

点赞(异步恳求)

点赞用Ajax实现,可对贴子也可对评论据赞,第一次点赞,第二次取消点赞(1:点过赞0:未点过赞),并显示点赞状态。按照返回的点赞状态和点赞数目进行正确的局部更新。Service方式如下:其中RedisKeyUtil是一个构造Rediskey的工具类,生成的key是由各个数组用逗号隔开的(Redis的惯用key命名方法)。我们通过set数据结构记录某个实体(如贴子)的点赞用户Id,再者用一个string数据结构记录某个用户获得的赞总量:这两个数据结构的操作须要用到Redis的事务一并实现。

将点赞相关信息存入Redis的数据结构set中。其中,key命名为like:entity:entityType:entityId,value即点赞用户的id。例如key=like:entity:2:246value=11表示用户11对实体类型2即评论进行了点赞,该评论的id是246某个用户的获赞数目对应的储存在Redis中的key是like:user:userId,value就是这个用户的获赞数目

我收到的赞

构建点赞功能:以用户为key,记录点赞数目,查询点赞数目

关注(异步恳求)

统计用户的关注数,粉丝数关注的对象可以是用户、帖子、评论。和点赞的区别在于:key中须要包含关注者和被关注者这两个变量。若A关注了B,则A是B的粉丝,B是A得目标

Service层定义follow和unfollow等方式,试举一例:你可以查看他人的关注列表,并对列表中的用户进行关注。这儿要用zset数据结构,在进行关注的人/粉丝列表显示的时侯,可以按照关注时间进行排序显示。

若A关注了B,则A是B的粉丝Follower,B是A的目标Followee关注的目标可以是用户、帖子、题目等,在实现时将这种目标具象为实体(目前只做了关注用户)将某个用户关注的实体相关信息储存在Redis的数据结构zset中:key是followee:userId:entityType,对应的value是zset(entityId,now),以关注的时间进行排序。例如说followee:111:3对应的value(20,2020-02-03-xxxx),表明用户111关注了一个类型为3的实体即人(用户),关注的这个实体id是20,关注该实体的时间是2020-02-03-xxxx同样的,将某个实体拥有的粉丝相关信息也储存在Redis的数据结构zset中:key是follower:entityType:entityId,对应的value是zset(userId,now),以关注的时间进行排序

关注列表,粉丝列表

**缓存优化**

下边用Redis缓存进行了三处地方的性能优化。

取代session储存验证码理由如下:验证码须要频繁的访问和刷新,对性能要求较高验证码只须要暂存,不须要常年保存如未来涉及到分布式布署,能防止session共享的问题。

储存登陆账簿每次对网站的恳求就会通过拦截器查询用户的登陆账簿,因而考虑到账簿的时效性和常常性,可以改为用Redis储存账簿而不是用数据库储存。

储存用户信息同上,每次对网站的恳求就会通过拦截器获取用户的登陆账簿,之后按照账簿获取用户信息以保持登入状态,访问频度十分高。因而对于findUserById这个方式,有必要做Redis缓存:这样,整个拦截器所调用的方式就不会涉及到数据库了。但用户信息的储存会涉及到Redis和MySQL的缓存不一致问题,须要解决:1.优先从缓存中取值2.取不到时初始化缓存数据3.数据变更时去除缓存信息关于缓存不一致问题,有好多解决方式,这儿采用的是:当数据变更时,先更新数据库,再删掉缓存。其实无论是先更新数据库还是先删掉缓存,就会有并发访问情况下的不一致问题和第二步操作失败的问题。前一个问题可以采用延后双删策略来解决。后一个问题可以用重试机制来解决

发送系统通知

用Kafka做消息队列也能对系统进行优化。(Kafaka建立TB级异步消息系统)

原本Controller层的一些实现逻辑,可以转移到EventConsumer类中实现,享有消息队列异步削峰前馈的优势。比如系统通知的实现,本身和点赞、关注、评论的逻辑关联不强,且这种动作频繁发生,因而可以通过异步实现,提升性能。在诸如前面用到的ES数据库的更新也可以用消息队列来实现。触发风波:

**

显示系统通知**

至于系统消息的查看、列表等实现,则基本与私信的实现差不多。(数据库中from_id为1表示这不是普通私信而是系统通知)显示评论,点赞,关注三种类型的通知在页面背部显示所有未读消息的数目,也显示各种类型主题未读消息数目

搜索

Elasticsearch目前性能最好,最流行的分布式搜索引擎。支持对各种类型数据的检索,搜索速率十分快,可以提供实时的搜索服务,每秒可以处理PB级海量数据Elasticsearch相当于特殊的数据库,搜索就是对这个数据库的搜索。ElasticsearchService须要完成三个方式:save、delete和search,重点是search:发布风波:发布帖午时,通过消息队列将贴子异步地递交到Elasticsearch服务器为贴子降低评论时,通过消息队列将贴子异步地递交到Elasticsearch服务器搜索服务:将贴子保存到Elasticsearch服务器从Elasticsearch服务器搜索贴子从Elasticsearch服务器删掉贴子(当贴子从数据库中被删掉时)显示搜索结果:在控制器中处理搜索恳求,在HTML上显示搜索结果

类似的,置顶、加精也会触发发贴风波,就不再图上面画下来了。

权限控制

SpringSecurity是一个专注于为java应用程序提供身分认证和授权的框架,它的强悍之处在于它可以轻松拓展以满足自定义的需求。可以避免各类功击,如会话固定功击,点击绑架,csrf功击等废弃了之前采用拦截器实现的登陆检测,使用SpringSecurity框架来进行统一认证授权管理。Security底层原理蛮复杂的,我们这儿对其进行简单的使用。而用户认证方面,因为我们采用自定义的认证形式,因而无需采用Security提供的方法,但我们认证信息仍须要存到SecurityContext里,拦截器须要改进认证信息会在preHandle加入,afterafterCompletion处删掉,logout时也会删掉。关于认证信息何时删掉的思索:每次在afterCompletion时删掉:Security是基于Filter的,假如每次在afterCompletion时删掉,这么上次恳求时首先抵达Filter,因为没有认证信息,会被判断权限不够,直接跳转到登陆页面,虽然早已登陆。在afterCompletion时不删掉,只在logout时删掉:假如这样即使能保证权限与恳求匹配,并且因为登陆账簿会过期,用户信息会被消除,但认证信息却不会被消除,用户就算没登陆也能访问,即便不合常理。因而正确做法为afterafterCompletion处假如没有用户信息就删掉认证信息,logout时也删掉。

置顶加精删掉(异步恳求)

版主可以看见置顶,加精操作;管理员可以看见删掉操作;点击置顶更改贴子的类型,点击加精,删掉更改贴子的状态Thymeleaf有对Security的支持,可以从SecurityContext从获得权限信息。因而赋于用户不同的权限(置顶、加精、删除)

网站数据统计

独立访客UV

使用Redis的HyperLogLog数据结构去实现,该数据结构的特性是占用显存很小,但会损失一定的统计精度。使用拦截器,旅客每次访问时Redis记入该ip。统计区间UV:整理日期范围内的key,合并数据(union去重生成新数据),返回统计结果日活跃用户DAU

类似UV统计,只是DAU统计操作的是bitmap:统计区间内的DAU,整理日期范围内的key,or运算权限管理(SpringSecurity)

热帖排名

使用Quartz实现热帖排名和更新,相比JDK的ScheduledExecutorService和Spring的ThreadPoolTaskScheduler的优势:Quartz实现定时任务所依赖的参数是保存在数据库中,数据库只有一份,所以不会冲突。而ScheduledExecutorService和ThreadPoolTaskScheduler是基于显存的,在分布式环境中,多台服务器会重复执行定时任务,形成冲突。热帖排名和更新的实现逻辑如下:贴子的score数组估算方式自定义为log(精华分+评论数*10+点赞数*2+收藏数*2)+(发布时间-牛客纪元)。一旦涉及到上述score会变化的操作,如贴子被设为精华,或贴子有新的评论等,贴子id会被装入Redis的set中。每隔一段时间执行定时任务,会从set中pop贴子id下来进行分数的刷新。每次发生点赞(给贴子点赞)、评论(给贴子评论)、加精的时侯,就将这种贴子信息存入缓存Redis中,之后通过分布式的定时任务SpringQuartz,每隔一段时间就从缓存中取出这种贴子进行估算分数。贴子分数/热度估算公式:分数(热度)=权重+发贴距离天数

 private void refresh(int postId) {
        DiscussPost post = discussPostService.findDiscussPostById(postId);

        //算分的时候发现被管理员删了
        if(post == null) {
            logger.error("待刷新帖子不存在: id = " + postId);
            return;
        }

        //是否精华
        boolean wonderful = post.getStatus() == 1;
        //评论数量
        int commentCount = post.getCommentCount();
        //点赞数量
        long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId);

        //计算权重
        double w = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2;
        //分数 = 权重 + 距离天数
        double score =
                Math.log10(Math.max(w, 1)) + (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24);

        //更新帖子分数
        discussPostService.updateScore(postId, score);
        //同步搜索数据
        post.setScore(score);
        elasticSearchService.saveDiscussPost(post);
    }

热帖页面按照贴子的score进行排序。

生成长图

使用wkhtmltopdf将牛客网html转换成pdf

将文件上传至云服务器

头像上传到七牛云进行储存,spring对此提供了支持。

优化网站性能

在实际的布署中,服务器缓存因为就在服务器本机内,因而对性能的提高相比Redis更高。对于热帖等与用户状态无关的内容可以存到本地缓存中。本项目用Caffeine实现,主要缓存贴子列表和贴子总量。须要定义两个缓存管理器,一个针对热帖列表,一个针对贴子总量,可以通过@PostConstruct初始化缓存用JMeter做下小测试,30w数据的情况下,性能有数倍到数十倍的提高。

压力测试:

使用JMeter工具做压力测试,先在数据库里打算大量数据;在测试方式中诸如我初始化30000条数据,先进行不加缓存测试双击启动bin目录下的jmeter.bat添加-》线程-》线程组填写线程名称,线程数,循环次数,持续时间等添加-》取样器-》Http恳求填写服务器名称(ip),端标语,http恳求形式,路径,编码类型等添加-》定时器-》统一随机定时器填写线程延后属性(ms)添加-》监听器-》聚合报告在聚合报告看压力测试结果:主要看吞吐量那一栏(多测试几次,且要保证异常为0%)之后加缓存进行测试启用缓存,与上述一样进行压力测试,可以发觉有缓存的吞吐量较高

项目布署

须要更改一些路径以适配linux环境。为便捷切换,可以使用两套配置文件。Spring容许项目中存在多套配置文件(例如:开发时一套,布署时一套,测试时一套),通过一个开关来启用哪一套配置文件

整个项目布署到阿里云ECS上(2cpu/4g/CentOS),布署步骤为:

下载相关包:jdk,maven,MySQL、Redis、Kafka、Elasticsearch,Wkhtmltopdf,Tomcat,Nginx。处理项目:把项目布署到Tomcat下:Build-》BuildArtifact-》community:war->Build,项目被打成war包丢到target包下,将war包上传到tomcat的webapps文件夹中,布署成功。

点赞关注网站