⽜客⾼级项⽬课之⽜客讨论区——项⽬总结
⽜客讨论区项⽬总结
本项⽬是个⼈独⽴开发的后端项⽬,这要涉及到Spring、SpringMVC、Mybatis的整合,以及SpringBoot去简化Spring的配置开发。
主要的技术点:
1. 登录注册功能:使⽤kaptcha去⽣成验证码,使⽤邮件完成注册,Redis优化验证码的保存,解决分布式session问题
2. 使⽤拦截⽤户请求,将⽤户信息绑定在ThreadLocal上
3. 构建Trie数据结构,实现对发表帖⼦评论的敏感词过滤
4. ⽀持对帖⼦评论,也⽀持对评论进⾏回复
5. 利⽤AOP对service的业务代码实现⽇志记录
6. 利⽤Redis的zset并结合Redis实现点赞关注的功能
7. 点赞关注后的系统通知,实时性不需要特别⾼,使⽤kafka实现异步的发送系统通知
8. 使⽤ElasticSearch实现对帖⼦的搜索功能,以及结果的⾼亮显⽰
9. SpringQuartz实现定时任务,完成热门帖⼦的分数计算模块
10. 使⽤本地缓存Quartz缓存热门帖⼦优化热门帖⼦页⾯,提⾼了QPS(10 - 200)
项⽬准备
准备⼯作就是⼀些⼯作环境的配置,IDE使⽤IntellijIDEA,具体的框架的版本与配置,以及mysql,redis,kafka,es的安装与配置,视频⾥都会有详细的介绍,kafka,es在mac系统上的安装我也有写博客介绍,在此就不必再做详细的说明。下⾯介绍⼀些关于此项⽬的主要模块后端代码的实现部分。
注册与登录功能的实现
注册是登录功能是每个⽹站最基本的功能,实现的主要难点我觉得在于怎么解决分布式Session问题,密码安全问题,以及怎么优化登录的问题。
⽤户表实现
id username password salt email type status activation_code header_url create_time 密码MD5加密
为了保证安全,密码不能明⽂的在⽹络中进⾏传输,也不能以明⽂的形式存到数据库中。
存在数据库的密码 = MD5( 密码 + salt ) 防⽌密码泄露
会话管理(分布式Session问题)
由于Http是⽆状态的,每次的http请求之间信息不共享,为了保证⽤户每次请求不⽤重新输⼊账号密码,保存⽤户的登录状态,就会有session和cookie这样的机制,去保存⽤户登录信息,但是在分布式部署的时候就会存在session共享的⼀个问题。
由于现在⽹站基本是多台服务器分布式部署的,如果将⽤户信息存到session中,⽽session是存到服务器上,在分布式环境下,由于各个服务器主机之间的信息并不共享,将⽤户信息存到服务器1上,同⼀个⽤户的下⼀个请求过来的时候,由于nginx的负载均衡策略,去请求了服务器2,就不到之前的session了。下⾯介绍⼏种分布式Session问题的解决策略。
粘性session:同⼀个ip分给同⼀个服务器,很难做负载均衡
同步Session:当⼀个服务器创建了session之后,会将该Session同步给其他
服务器。服务器之间耦合,加⼤服务器之间的同步开销
Session服务器:专门⼀个服务器管理Session,这台服务器是单体的,万⼀挂掉,有安全隐患
将客户端会话数据不存到Session中⽽是存到数据库中:
关系型数据库性能较慢
存到redis中(项⽬中采⽤的⽅式)
Kaptcha⽣成验证码
Loginticket⽣成凭证记录登录状态
本项⽬中先采⽤将⽤户登录信息存到数据库的login_ticket表中,后续采⽤存到redis中优化。
V1 将⽤户登录凭证ticket存到mysql的login_ticket表中
登陆成功的时候⽣成登录凭证,⽣成Loginticket往数据库login_ticket存,并且被设置为cookie,下次⽤户登录的时候会带上这个ticket,ticket是个随机的UUID字符串,有过期的时间expired和有效的状态status。
⽤login_ticket存储⽤户的登录信息,每次请求会随着cookie带到服务端,服务端只要与数据库⽐对携带的ticket,就可以通过表中的used_id字段查到⽤户的信息。
⽤户退出时将status更改为0即可。
id user_id ticket status expired
V2:使⽤Redis优化登录模块
使⽤Redis存储验证码
1. 验证码需要频繁的访问与刷新,对性能要求⽐较⾼
2. 验证码不需要永久保存,通常在很短的时间后就会失效(redis设置失效时间)
3. 分布式部署的时候,存在Session共享的问题(之前验证码是存到session⾥⾯,使⽤redis避免session共享问题)
Key Value
kaptcha:owner string
owner : 由于此时⽤户还未登录,owner为临时⽣成的凭证,存到cookie中发送给客户端。
登录的时候从cookie中取值构造redisKey,再从redis中取值。并与⽤户输⼊的验证码进⾏⽐对。
1.最初是直接将验证码字符串存到session当中,每次都是从session中获取验证码字符串的值在进⾏判断,这同样会出现分布式
session的问题,⽐如说刷新验证码是⼀次请求,此次请求将验证码存到了服务器A的session当中,但是在点击登录按钮,去触发登录请求是,则将此次请求转到了服务器B,⽽服务器B并没有存储验证码的session,就会出现⽆法判断的问题。
2. ⽣成验证码的时候⽤户还未登录,此时随机⽣成⼀个kpatchowner字段标明当前⽤户,并存到cookie中,同时将kaptchowner字
符串最为redis的key,具体的验证码的值作为redis的value。
3. ⽤户进⾏登录验证的时候,先从cookie中取出kpatchowner字段,得到唯⼀标明属于此⽤户验证码的rediskey,再从redis中取出
验证码真正的⽂本,然后与⽤户的输⼊进⾏⽐对,判断你验证码是否正确。
使⽤Redis存储登录凭证,作废login_ticketsession如何设置和读取
处理每次请求的时候,都要从请求的cookie中取出登录凭证并与从数据库mysql中查询⽤户的登录凭证作⽐对,访问的频率⾮常⾼(原来登录凭证ticket是存到mysql⾥⾯,ticket如果⽤redis存,mysql就可以不⽤存了,login_ticket可以作废)
Key Value
ticket:ticket Loginticket(Json字符串)
第⼆个ticket为实际的ticket字符串,Loginticket被序列化为Json字符串。
退出登录的是,需要让登录凭证失效,此时根据key将Loginticket取出来,在更改其状态为1失效态,再重新存回Redis中。 (不删除可以保留⽤户登录的记录)
使⽤Redis缓存⽤户信息
处理每次请求的时候,都要根据登录凭证查询⽤户信息,访问的评率⾮常⾼(每次请求的时候需要根据凭证中的⽤户id查询⽤户)
查询User的时候,先尝试从缓存中取值,如果没有的话,就需要初始化,有些地⽅会改变⽤户数据,需要更新缓存,可以直接把该⽤户的缓存删除,下⼀次请求的时候发现没有⽤户的信息,就会重新查⼀次再放到缓存中
的使⽤
声明(实现HandleInterceptor)并在spring @Configuration中配置拦截信息
在请求开始时查询登录⽤户
在本次请求中持有⽤户数据
使⽤Interceptor来拦截所有的⽤户请求,判断请求中的cookie是否存在有效的ticket,如果有的话就将查询⽤户信息并将⽤户的信息写⼊ThreadLocal在本次请求中持有⽤户,将每个线程的threadLocal都存到⼀个叫做hostHolder的实例中,根据这个实例就可以在本次请求中全局任意的位置获取⽤户信息。
redis是存储⽤户登录的状态,是在⽤户登录之后,跨不同的请求的,⽽使⽤ThreadLocal具体针对的是⼀次请求,在这次请求中去存储⽤户信息,⽅便程序的开发,⽐如说我请求了帖⼦详情页⾯,去做评论或者回复,就可以直接从threadLocal中取到⽤户的信息,进⾏编码。
发布帖⼦与敏感词过滤
使⽤AJAX异步发帖
*    AJAX - Asychronous JavaScript and XML
*    异步的JavaScript与XML, 不是⼀门新的技术,只是⼀门新的术语
*    使⽤AJAX,⽹页能够将改变的量更新呈现在页⾯上,⽽不需要刷新整个页⾯
*    虽然X代表XML,但是⽬前JSON的使⽤⽐XML更加普遍
发布帖⼦的时候需要对帖⼦的标题和内容进⾏敏感词,通过Trie实现敏感词过滤算法,过滤敏感词⾸先需要建⽴⼀颗字典树,并且读取⼀份保存敏感词的⽂本⽂件,并⽤⽂件初始化字典树,最后将敏感词作为⼀个服务,让需要过滤敏感词的服务进⾏调⽤即可。
具体实现见下:
防⽌xss注⼊:防⽌xss注⼊直接使⽤HTMLUtils的⽅法即可实现。
发表评论以及私信
评论
评论的是每⼀个帖⼦都评论,并且⽀持对评论进⾏评论,也就是评论的回复,
能够显⽰评论的数量,具体的内容,以及评论⼈回复⼈等等。
评论表的实现:
id user_id entity_type entity_id target_id content status create_time
其中
Entity_type 评论的⽬标的类别 1:帖⼦ 2: 评论 ⽀持回复评论
entity_id 评论具体的⽬标
target_id 记录回复指向的⼈ (只会发⽣在回复中 判断target_id==0)
user_id 评论的作者
添加评论时:(将添加评论和更新评论数量放在⼀个事务中)使⽤spring声明式事务管理@Transactional实现
私信
私信是⽀持两个⽤户之间发送消息,有⼀个唯⼀的会话id,以⽤户id⼩的开头,id⼤的结尾⽐如12_111,以varchar的形式存储到数据库中,这个会话⾥可以有多条两个⽤户交互的消息。
私信表的实现
id form_id to_id conversion_id content status create_time
form_id 112 其中id = 1代表的是:系统通知
to_id 111
conversion_id 111_112(id⼩的在前)111与112 之间的会话
status 0:未读 1:已读 2:删除
复杂sql
这块的sql编写是相对⽐较复杂的,主要是在会话列表中显⽰每条会话中最新的消息内容。
显⽰未读消息的数量。根据时间顺序排列会话
查询当前⽤户的会话列表,针对每个会话只返回⼀条最新的私信
select id, from_id, to_id, conversion_id, content, status, create_time from message
where id in(
select max(id) from message
where status != 2
and from_id != 1
and (from_id = user_id or to_id = user_id)
group by conversion_id
)
order by id desc
limit 0, 10
同样这⾥也采⽤异步请求的⽅式发送私信,发送成功后刷新私信列表
Spring aop记录⽇志
aop实现对service层所有的业务⽅法记录⽇志
Aop是⼀种编程思想,是对OOP的补充,可以进⼀步提升效率
Aop解决纵向切⾯的问题,主要实现⽇志和权限控制的功能
aspect实现切⾯,并且使⽤Logger来记录⽇志。⽤该切⾯的切⾯⽅法来监听controller
主要针对的是控制层controller