Skip to content

Latest commit

 

History

History
113 lines (99 loc) · 11.6 KB

模块介绍.md

File metadata and controls

113 lines (99 loc) · 11.6 KB

注册、登录模块

注册

  1. 检查是否允许注册。当用户点击页面顶部“登录”按钮,打开注册页面,允许注册时,输入账户、密码和邮箱,通过表单提交注册数据。
  2. 服务端对用户输入的信息进行检测。填入的信息不能为空,验证输入的账户是否已经存在,填写的邮箱是否已经注册过。
  3. 注册用户。用户传入的密码通过 MD5+salt 的方式加密,设置用户的类型为普通用户,用户的状态为未激活状态,设置用户的激活码,给用户随机分配系统自带的头像, 生成用户注册的时间。
  4. 激活注册账号。服务端利用模板引擎发送激活邮件至用户注册时填写的邮箱中。 用户点击邮件中的激活码,访问服务端的激活服务。通过验证激活码来验证激活是否成功。
  5. 利用kaptcha生成验证码,将验证码放入cookie中,并存到Redis里。

登录

  1. 登录方式分为论坛注册账户登录和第三方账号登录(github和qq)。
  2. 论坛内注册的账户登录:提交账户信息后,服务端会先验证用户填入的信息,其中验证码在表现层就要判断,验证码不对密码和账号就不要判断了; 登录信息无误后,生成登录凭证(loginTicket),将其按key=ticket,value=loginTicket存到Redis里,并将登录凭证放入cookie中。每次访问浏览器时会从Redis中通过ticket取出loginTicket,并判断登录状态。此处Redis存储实现了分布式的部署。
  3. 第三方登录:选择第三方登录后,会去相应的第三方服务器中获取用户信息并返回给服务端。服务端会通过返回的用户信息中的openId来判断该用户是否已经注册。 若注册过即取出并登录,否则注册并登录。同时生成登录凭证,放入cookie里,存到Redis中,用户下次访问时判断是否登录。

个性化推荐模块

  1. 整体上本模块采用"推"和"拉"的方式并存。
  2. 当用户点赞,评论,发布博客时,会将这些"动态"封装成feed对象存入数据库,同时将这个动态发给所有的粉丝(从数据库中找到所有的粉丝,筛选最近n天登陆过的人,然后将此feed存入他们对应的redis的timelineKey中,即 每个用户的redis的timelineKey中存放着自己关注的对象的动态)
  3. 当用户登录后,redis中有值则从redis中对应的key中取,否则先从redis中获取所有的关注的人,然后从数据库中遍历搜索。 至此,获得了用户关注的人的最近动态
  4. 当用户每查看一个帖子时,就会将这个帖子的标签存到redis对应的tagKey中。
  5. 最终,用户的个性化页面展示顺序即为:
    1. 用户关注的人发送的帖子
    2. 用户关注的人点赞的帖子
    3. 用户关注的人评论的帖子
    4. 用户最近查看的标签对应的帖子且发布时间是近期(否则用户看了两个"多线程"的帖子,然后向此用户推荐了网站所有的多线程,显然不合适,所以只推荐最近发布的"多线程"的帖子)
    5. 再按照分数递减的顺序

注意点

  1. 我们为timelineKey设置了生存时间,如小p关注了小f,需要判断小p N天内是否登录,
    • 步骤
      • 如果登录则小f发布的帖子,进入了小p的timelineKey,
      • 如果小p 5天没登录了,此时小p的timelineKey将被自动删除,则从此小f的点赞评论都不需要告诉小p(占redis内存),
      • 只有当小p再次登录时,则小f发布的帖子,评论点赞才会进入小p的timelineKey,以此解决redis内存。
    • 那么我们如何判断小p N天内登录过呢?
      • redis里有个session登录凭证,但这个时间记住密码是30天,不记住密码则是7天,显然不合适,所以我们为redis中增加一个字段
      • 每次小p登录,在redis中为user:login_5:{userId}随意设置一个值,过期时间为5天
      • 当小p再次登录时, 再次设置,此时相当于过期时间刷新
      • 当小f登录时,判断 user:login_5:{userId} 是否存在,存在则向timelineKey中push(并写数据库),否则就只写数据库
        • 此时写数据库是异步的,所以不会影响正常逻辑,且当小p 5天内登陆过,则5天内再次登录可能性很大,当他再次登录,直接从redis中取,速度很快,且对mysql压力小
  2. 如果我们最近查看了"多线程",那么分页查询第一页一定是0-10,这时候我们看的帖子的标签就会被redis记录,此时如果我们点击第二页,则从redis中的tagKey取值时,第二页的查询条件就会改变, 这时候可能第一页查看过的数据,第二页又会出现,或者是有些数据,永远看不到,即我们需要保证分页查询时,每次查询条件不变,所以我们设置了两个key,持久化key(persistence)和最近的key(latest), 这时候我们每次查询都从持久化key中取,而每次查询第一页时,将持久化key置换为最新的key,这样即可以保证每次查询条件相同,且是伪最新的标签。

缓存模块

  1. 本项目的缓存介绍
    • 本项目采用2级缓存机制
    • 第 1 层为caffine本地缓存层,速度快,稳定(和JVM共生死)
    • 第 2 层为redis集中式缓存层,速度次之,集中式存储
    • 第1.5层为本地缓存同步层,即集群模式下对于一个caffine的修改,利用本地缓存同步层同步到其他本地缓存
  2. get方法
    • 先判断1级缓存有没有,有则直接返回
    • 1级缓存没有则判断2级缓存有没有,有则返回,同时通过多线程方式设置到1级缓存
    • 如果2级缓存也没有,则通过数据库查,数据库查到了,则返回,同时通过多线程方式设置到1级缓存和2级缓存
    • 如果数据库也没有,则直接返回空,同时通过异步方式设置到1级缓存和2级缓存一个特殊空标记,防止恶意攻击
  3. set方法
    • 将一级缓存中的数据修改了
      • 同时消息发布其他的一级缓存,进行删除(只消息发布key,让其他的caffine把这个key删了)
    • 继续修改二级缓存中的数据
    • 修改数据库
  4. 本项目对于所有的帖子都以key:{帖子Id} value:{帖子内容} 的形式进行存储,这样针对于一些其他的组合操作,如我们希望存储某个标签下的所有帖子, 则我们只需要存储多个id,然后通过id继续查询缓存即可,这样较为灵活。
  5. 如4所言,我们可以在搜集标签时,将属于标签的多个帖子的ID一并存储下来,这样当点击标签时,可以较迅速的找到属于这个类别下的帖子,而减少对数据库的%%全表扫描亚丽
    • 但这又会引入新的问题,即标签必须实时更新,所以//todo 下一步,引入redis,将标签对应的帖子序号在redis中进行存储
  6. 思考
    • 6.1 能否将1级缓存和2级缓存的回写使用异步?
      • 这种情况下1级缓存如果查询不到,则去2级缓存查,然后消息队列异步发送,但此时只有一个caffine可以接收到消息,然后进行缓存,即第一次访问找一级缓存,没找到,设置一级缓存,结果设置到其他机器上了,这样用户可能多次请求, 多次都要设置一级缓存,
      • 所以我们可以采取两种方式:
          1. nginx hash方式,这样异步就更不行了,因为必须要设置到本台机器。
          1. 1级缓存的设置用消息发布订阅,即一台服务器的1级缓存没找到,则从2级缓存找,然后通过消息发布让所有的机器都进行存储,这样可能会导致caffine中数据存储过多。我觉得这样也不是很好。
        • 综上,我觉得可以使用nginx hash + 多线程设置的方式,即某个用户的每次请求都打到同一台机器上,如果这台机器caffine没有,则查询2级缓存,然后设置到这台服务器的一级缓存,这样当这个用户下次继续请求时,caffine就有了。
      • 此时如果修改呢?那将一级缓存和2级缓存修改了,再消息发布将其他caffine删了,其他caffine本来就没有,所以也没有错
      • 而本项目使用消息发布订阅,原因是因为没有做nginx hash,所以会导致用户的一次访问,设置到1级缓存了,下次访问1级缓存又没有,又设置到1级缓存了,此时可能多个caffine里有数据,所有采用消息发布订阅进行修改。当然,在这种情况下,用异步和子线程进行回写缓存,效果差不多,都会产生多次写一级缓存的情况。
    • 总结:
      • 第一阶段: set方式中同步方式:用户响应慢
      • 第二阶段: 消息队列发送消息,不一定自己这台应用服务器收到,这样可能多次访问自己这台应用服务器,一直是其他的应用服务器收到消息并重复覆盖
      • 第三阶段: 消息发布订阅模式,所有应用服务器都置数据,这样会导致在用户量大的情况下,应用服务器内存压力大
      • 第四阶段: 使用nginx hash法让固定用户请求发送至同一应用服务器,这样减少应用服务器内存压力,这种情况下异步的消息队列和发布订阅模式都不合适(前者不是自己接受,后者存的数据太多了)
      • 第五阶段: 多线程设置,本应用服务器的线程只能操作自己的缓存。
      • 第六阶段: 线程池操作,基于全局线程池可用自定义线程池类(static 线程池)和spring bean来操作。

主页热门标签显示模块

  1. 用户发布帖子时,都会附带一个tag标签
  2. 使用quartz,每隔3个小时统计一次。遍历所有帖子,统计所有出现的标签。
  3. 根据每个标签匹配所有帖子,统计含有此标签的帖子的总数,计算帖子总分,组成一个tag。这里需要注意,我们在查询某标签(如算法)对应的帖子时, 查询数据库的算法使用了“%XX%”(如%算法%)的匹配条件,这会得到其他相关联的标签(如“算法总结”等)。
  4. 使用treeSet对这些标签按照总分排序,然后选出N个帖子存储在tagCache中,每当用户访问主页面时,就从TagCache中去取
  5. 当帖子数量增多时,统计时间也可以设置为每天晚上3点,夜深人静偷偷统计。(统计帖子的个数,有点误差可以接受,所以一天统计一次似乎也可行)

HostHolder和TagCache的设计解决并发问题

  1. HostHolder和TagCache两个类都分别有一个成员变量User类型的users和List类型showTags,在高并发环境下会存在并发问题。
  2. 对于HostHolder的设计,我们使用了ThreadLocal解决了并发问题;
  3. 对于TagCache的设计,由于ArrayList是线程不安全的,首先舍弃;其次考虑了线程安全的Vector,但是Vector底层是在读写上加Synchronized, 对于多读少写的环境而言效率很低;再其次考虑了Collections.SynchronizedList(),其底层也是给set和get加Synchronized,同样效率低;最后用CopyOnWriteList来实现的,其读写分离的思想十分适合 多读少写的环境,巧妙的解决了并发问题,且效率高。