Spring boot项目(问答网站)之timeline的推拉两种模式

Timeline介绍

所谓timeline就是当用户打开主页看到的随着时间轴发生的一系列时间的整合,主要包含:

  • 关注用户的最新动态
  • 热门推荐
  • 广告推荐整合等等.

推、拉模式

  1. 推模式: 当一个用户关注了或者评论了一个问题或用户,触发事件,将会将这一动态广播给该用户所有的粉丝
  2. 拉模式: 用户由于某种行为触发事件后,不会广播给每一个粉丝,只有当粉丝主动查询该用户最近动态时,才从缓存中读取,组建timeline内容。
  3. 推拉结合:当一个用户的粉丝数目过多时,推模式会对后台造成很大压力,浪费存贮空间,特别有些粉丝很有可能已经很久未登录;同样的,当一个用户关注的人特别多,或者同时有很多用户同时查询同样的动态时,对后台读取数据压力特别大。于是,在大多数大规模网站采用一种推拉结合的模式,即是对活跃的用户采用推模式,对不活跃用户采用拉模式,这样就缓解了后端的压力,同时满足了大多数用户的需求。

Timeline的存储

在项目中采用timeline的新鲜事(最新动态)统一存储的模式,存储在在mysql中feed表中,记录下来用户新鲜事的核心数据(例如,如果用户关注了一个问题,那么数据就会包含:用户id,用户的头像、问题id,问题的title,问题的url等)。
在推模式下,每一个用户都应该有自己的timeline数据表,当他关注的用户产生新鲜事feed时,需要新鲜事的id存放在自己的timeline数据表中。在本项目中,我们在redis中存储新鲜事的id,键与值对应关系为:key(TIMELINE:用户id) , value(新鲜事在feed表中的id)。
另外,在后端timeline应该秩保存新鲜事的核心数据,不应该保存渲染它们的模板(前端的东西),当数据访问新鲜事列表时,从后端查询得到新鲜事核心数据,到达前端时应该针对不同的数据信息调用不同的模板去整合渲染。

整体路线

下图表现了整合timeline实现路线,从事件触发到事件处理,然后到timeline存储,以及调用不同模板去渲染整合:
在这里插入图片描述

实现代码

采用经典的三层结构,即model、service以及controller。

  • 首先,是model层,新建java普通对象Feed,并构造对应的setter以及getter,同样的在mysql中创建Feed表,包含与Feed类一致的字段,使用mybatis将数据库与Feed关联。需要注意的是,为了方便保存新鲜事的核心数据,我们将核心数据保存成json串的形式,但是这样在前端读取json会很不方便,因此在本项目中创建一个JSONObject对象,当创建data时,同时也会将json对象化,然后在创建一个get方法,通过key就可以获得保存在json串中对应的核心数据,方便了前端模板读取数据。
public class Feed {private int id;private int type;private int userId;private Date createdDate;private String data;   //利用JSON串存储的private JSONObject dataJSON = null;   // 直接保存一个json对象,便于直接通过key获取在json串中对应的valuepublic int getId() {return id;}public void setId(int id) {this.id = id;}public int getType() {return type;}public void setType(int type) {this.type = type;}public int getUserId() {return userId;}public void setUserId(int userId) {this.userId = userId;}public Date getCreatedDate() {return createdDate;}public void setCreatedDate(Date createdDate) {this.createdDate = createdDate;}public String getData() {return data;}public void setData(String data) {this.data = data;dataJSON = JSONObject.parseObject(data);}/*** 通过json串中的key获取value,为了方便在以后在前端直接从json读取相应的信息* @param key* @return*/public String get(String key){return dataJSON == null ? null : dataJSON.getString(key);}
}
  • 然后,创建对应的DAO层以及Service层。
// DAO 层
@Mapper
public interface FeedDao {String TABLE_NAME = " feed ";String INSERT_FIELDS = " type, user_id, created_date, data ";String SELECT_FIELDS = " id, " + INSERT_FIELDS;/*** 向Feed表插入一条新鲜事* @param feed* @return*/@Insert({"insert into ", TABLE_NAME, " (", INSERT_FIELDS, " ) values (#{type},#{userId},#{createdDate},#{data})"})int addFeed(Feed feed);/*** 通过根据新鲜事的id查询对应的新鲜事* @param id* @return*/@Select({"select ", SELECT_FIELDS, " from ", TABLE_NAME, " where id=#{id}"})Feed getFeedById(int id);/*** 在拉模式下,根据用户的关注用户id列表查找用户的新鲜事* 由于比较复杂,通过在类相同路径的xml文件进行配置* @param maxId  设置最大的id数目* @param userIds 关注用户的id列表* @param count  用于分页显示* @return*/List<Feed> selectUserFeeds(@Param("maxId") int maxId,@Param("userIds") List<Integer> userIds,@Param("count") int count);
}

用于配置selectUserFeeds的xml文件为:

<mapper namespace="cn.dut.wenda.dao.FeedDao"><sql id="table">feed</sql><sql id="selectFields">id, user_id, type, created_date, data</sql><select id="selectUserFeeds" resultType="cn.dut.wenda.model.Feed">SELECT<include refid="selectFields"/>FROM<include refid="table"/>WHERE id &lt; #{maxId}<if test="userIds.size() != 0">AND user_id in<foreach collection="userIds" index="index" item="item" open="(" separator="," close=")">#{item}</foreach></if>ORDER BY id DESCLIMIT #{count}</select>
</mapper>
// Service层
@Service
public class FeedService {@AutowiredFeedDao feedDao;public List<Feed> getUserFeeds(int maxId, List<Integer> userIds, int count){return feedDao.selectUserFeeds(maxId, userIds, count);}public Feed getFeedById(int id){return feedDao.getFeedById(id);}public boolean addFeed(Feed feed){feedDao.addFeed(feed);return feed.getId() > 0;}
}
  • 最后,创建Controller层,通过controller层用户选择以推拉哪种模式查看timeline页面。
    @RequestMapping(path = "/pushfeeds", method = RequestMethod.GET)public String getPushFeeds(Model model){// 用户为登录时,将默认显示用户为0的timeline,在feedHandler中,每次新的feed都被添加到了用户0的timeline中int localUserId = hostHolder.getUser() != null ? hostHolder.getUser().getId() : 0;List<String> feedIds = jedisAdapter.lrange(RedisKeyUtils.getTimelineKey(localUserId), 0, 10);List<Feed> feeds = new ArrayList<>();for (String feedId : feedIds){Feed feed = feedService.getFeedById(Integer.parseInt(feedId));feeds.add(feed);}model.addAttribute("feeds", feeds);return "feeds";}/*** 拉模式下,用户通过查询feed表,找到自己关注的用户的最新动态,动态组建timeline* 做法是先将用户的关注对象在redis中查询出来,然后得到fellowees的ids,* 然后在feed表中找到这些用户的feed,最后返回timeline到前端显示* @param model* @return*/@RequestMapping(path = "/pullfeeds", method = RequestMethod.GET)public String getPullFeeds(Model model){// 已经在拦截器中添加了本路径,当为登录时发生未登录跳转int localUserId = hostHolder.getUser() != null ? hostHolder.getUser().getId() : 0;List<Integer> followees = new ArrayList<>();if (localUserId != 0){followees = followService.getFollowee(localUserId, EntityType.ENTITY_USER, Integer.MAX_VALUE);}
//        System.out.println(Arrays.toString(followees.toArray()));List<Feed> feeds = feedService.getUserFeeds(Integer.MAX_VALUE, followees, 10);model.addAttribute("feeds", feeds);return "feeds";}
  • 最重要最关键的一步,就是创建事件处理Handler,当用户触发事件时,需要在处理事件的Handler中进行处理,对应上面的路线图中的两个数据库操作。
    /*** 处理事件Handler* @param model*/@Overridepublic void doHandle(EventModel model) {// 事件触发后,会构造一个新的feed对象// 其中保存type是为了在前端调用不同的模板去渲染,比如评论问题和关注问题会根据type的不同来用不同的宏去渲染Feed feed = new Feed();feed.setType(model.getEventType().getValue());feed.setCreatedDate(new Date());feed.setUserId(model.getActorId());feed.setData(buildFeedData(model));if (feed.getData() == null){return;}
//        feedService.addFeed(feed);System.out.println(feedService.addFeed(feed));// 采用推模式,将用户所有的粉丝查找出来,然后进行广播// 在redis中针对每一个每一个粉丝,将用户的timeline在feed表中的id存在每一个粉丝对应的在redis存放timeline的队列中List<Integer> followers = followService.getFollower(EntityType.ENTITY_USER, model.getActorId(), Integer.MAX_VALUE);// 将用户0添加进来,为了用户为登录情况下,看到所有用户的timelinefollowers.add(0);for (int follower :followers){String timelineKey = RedisKeyUtils.getTimelineKey(follower);jedisAdapter.lpush(timelineKey, String.valueOf(feed.getId()));System.out.println(timelineKey + " : " + feed.getId());}}/*** 根据事件模型将在前端显示所用到的数据存放在json串中* @param eventModel* @return*/private String buildFeedData(EventModel eventModel){Map<String, String> map = new HashMap<>();User actor = userService.getUser(eventModel.getActorId());if (actor == null){return null;}map.put("userId", String.valueOf(actor.getId()));map.put("userHead", actor.getHeadUrl());map.put("userName", actor.getName());if (eventModel.getEventType() == EventType.COMMENT|| (eventModel.getEventType() == EventType.FOLLOW&& eventModel.getEntityType() == EntityType.ENTITY_QUESTION)){Question question = questionService.selectById(eventModel.getEntityId());if (question == null){return null;}map.put("questionId", String.valueOf(question.getId()));map.put("questionTitle", question.getTitle());return JSONObject.toJSONString(map);}return null;}

实现中出现的错误

  • 第一个,就由于采用前端使用freemarker渲染,前面已经说了对不同的核心数据采用不同的模板进行渲染,因此,需要使用freemarker中的macro进行调用选择,出现第一个错误:
freemarker.core.NonHashException: For "." left-hand operand: Expected a hash, but this has evaluated to a string (wrapper: f.t.SimpleScalar):
==> vo  [in template "feeds.html" at line 9, column 25]

显示错误地方代码为:

<#list feeds as feedvo><#if feedvo.type == 1><@comment_question vo="${feedvo}"></@comment_question><#elseif feedvo.type == 4><@follow_question vo="${feedvo}"></@follow_question></#if></#list>

原因为feedvo是一个复杂数据类型,而上面的写法,把它变成一个String类型,因此在宏里面不能对它进行复杂数据类型的操作,然后修改为下面代码问题解决。

<#list feeds as feedvo><#if feedvo.type == 1><@comment_question vo=feedvo></@comment_question><#elseif feedvo.type == 4><@follow_question vo=feedvo></@follow_question></#if></#list>
  • 第二个,在通过Service层中的getUserFeeds方法,根据关注用户id列表获取Feed表中的数据时,发现得到的Feed对象的id都为0.通过以下步骤找到问题以及解决方法:
    1、 检查Feed类里面的getter和setter方法缺少,发现完整
    2、检查DAO层selectUserFeeds,检查xml配置文件,发现了错误,但是修改后仍是0,继续检查
    3、最后从网上看到,在使用mybatis是要把mybatis配置文件的useGeneratedKeys字段设置为true,否则在利用mybatis中insert一条数据,不会自动将对应表中的id封装到对象的id属性上面,最后解决问题。

总结

对于大型网站的timeline应该采用推拉结合的模式,具体活跃用户的定义可以参照微博等知名网站方式,由于本项目只是身为初学者的我练习使用的,因此采用了推拉两种方式。另外还有许多不足之处,希望您指正,自己也会继续学习,谢谢。

本项目参照牛客网spring boot问答网站项目视频

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544356.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Bean放入Spring容器,你知道几种方式?

作者&#xff1a;三尺微命 一介书生来源&#xff1a;blog.csdn.net/weixin_43741092/article/details/120176466我们知道平时在开发中使用Spring的时候&#xff0c;都是将对象交由Spring去管理&#xff0c;那么将一个对象加入到Spring容器中&#xff0c;有哪些方式呢&#xff…

Spring boot项目(问答网站)之Python学习基础篇

简介 当问答网站基本框架搭建完毕之后需要一些初始的数据来进行填充&#xff0c;因此选用Python爬虫的方式&#xff0c;从网上截取一些资料信息&#xff08;当然是自己做项目使用&#xff0c;非商用&#xff09;放入到项目网站上面。这篇主要是关于Python基础知识的学习笔记。…

Spring Boot Admin,贼好使!

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;Spring Boot Admin&#xff08;SBA&#xff09;是一个开源的社区项目&#xff0c;用于管理和监控 Spring Boot 应用程序。应…

JAVA基础之容器基础内容

Java Collections框架 Java Collections框架中包含了大量的集合接口以及这些接口的实现类和操作它们的方法&#xff0c;具体包含了Set(集合&#xff09;、List(列表)、Map(键值对)、Queue(队列)、Stack(栈)等&#xff0c;其中List、Set、Queue、Stack都继承了Collection接口。…

更快的Maven构建工具mvnd和Gradle哪个性能更好?

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;Maven 作为经典的项目构建工具相信很多人已经用很久了&#xff0c;但如果体验过 Gradle&#xff0c;那感觉只有两个字“真香…

SpringBoot + ShardingSphere 秒级分库分表!

Spring Boot 作为主流微服务框架&#xff0c;拥有成熟的社区生态。市场应用广泛&#xff0c;为了方便大家&#xff0c;整理了一个基于spring boot的常用中间件快速集成入门系列手册&#xff0c;涉及RPC、缓存、消息队列、分库分表、注册中心、分布式配置等常用开源组件&#xf…

git reset, git checkout, git revert 区别 (译)

博客原文地址: http://blog.mexiqq.com/index.php/archives/3/题记&#xff1a;团队中大多数成员使用 sourceTree 和 github 两款 git 工具&#xff0c;然而大家对于图形化工具提供的 reset,checkout,revert 功能点并不是很了解&#xff0c;甚至于混淆,然后凭借猜测去使用。功夫…

Redis笔记之基本数据结构 动态字符串SDS

简单动态字符串 传统上的C语言的字符串表示是以空字符结尾的字符数组&#xff08;C字符串&#xff09;&#xff0c;redis自己实现一个动态字符串&#xff08;SDS&#xff09;&#xff0c;两者之间的区别以及使用SDS的好处有&#xff1a; 结构不同。C字符串以空字符结尾的字符…

扯一把 Spring 的三种注入方式,到底哪种注入方式最佳?

1. 实例的注入方式首先来看看 Spring 中的实例该如何注入&#xff0c;总结起来&#xff0c;无非三种&#xff1a;属性注入set 方法注入构造方法注入我们分别来看下。1.1 属性注入属性注入是大家最为常见也是使用最多的一种注入方式了&#xff0c;代码如下&#xff1a;Service p…

Redis笔记之基本数据结构 链表

链表 链表具有空间存储不连续&#xff0c;增删节点快的优点&#xff0c;因此redis在列表键、发布与订阅、慢查询、监视器等使用了链表作为底层实现。由于C语言中没有内置的链表实现&#xff0c;因此redis自己进行了实现。 双向链表。每个listtNode都有perv和next指针&#x…

SpringCloud组件:Ribbon负载均衡策略及执行原理!

大家好&#xff0c;我是磊哥。今天我们来看下微服务中非常重要的一个组件&#xff1a;Ribbon。它作为负载均衡器在分布式网络中扮演着非常重要的角色。本篇主要内容如下&#xff1a;在介绍 Ribbon 之前&#xff0c;不得不说下负载均衡这个比较偏僻的名词。为什么说它偏僻了&…

Redis笔记之基本数据结构 字典

字典 符号表、关联数组或者映射&#xff0c;有点类似于java中的map&#xff0c;用于保存键值对key-value。字典中的键key是独一无二的。底层实现为哈希表。下面进行简述&#xff1a; 哈希表。哈希表主要包含table数组、size、sizemask以及used。table用于保存哈希表节点&…

【零基础学习iOS开发】【02-C语言】02-第一个C语言程序

本文目录 前言一、编写第一个C语言程序-Hello World二、编译程序三、链接程序四、运行程序五、总结六、学习建议七、clang指令汇总回到顶部前言 前面已经唠叨了这么多理论知识&#xff0c;从这讲开始&#xff0c;就要通过接触代码来学习C语言的语法。学习任何一门语言&#xff…

安卓平板体验Java开发,还能白嫖一年阿里无影云,真香!(内含白嫖方法,人人可领)...

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;阿里无影云早有耳闻&#xff0c;前两天看朋友发体验照片&#xff0c;可能是程序员天生爱折腾的特性又发挥作用了&#xff0c…

你知道group by的工作原理和优化思路吗?

前言 日常开发中&#xff0c;我们经常会使用到group by。亲爱的小伙伴&#xff0c;你是否知道group by的工作原理呢&#xff1f;group by和having有什么区别呢&#xff1f;group by的优化思路是怎样的呢&#xff1f;使用group by有哪些需要注意的问题呢&#xff1f;本文将跟大家…

关Jquery判断input type=checkbox元素是否被选中的判断

2019独角兽企业重金招聘Python工程师标准>>> 在用到复选框的时候&#xff0c;想在js中判断chekbox是否被选中 <input name"isPermit" id"isPermit" type"checkbox"> 百度了很多的判断方法 1、 if($("#isPermit").att…

Redis夺命十二问,你能扛到第几问?

Redis是面试中绕不过的槛&#xff0c;只要在简历中写了用过Redis&#xff0c;肯定逃不过。今天我们就来模拟一下面试官在Redis这个话题上是如何一步一步深入&#xff0c;全面考察候选人对于Redis的掌握情况。小张&#xff1a;面试官&#xff0c;你好。我是来参加面试的。面试官…

bzoj 1192

http://www.lydsy.com/JudgeOnline/problem.php?id1192 好像学过一个东西&#xff1a; [0..2^(N1)-1]内的数都的都可以由2^0,2^1,...,2^N这N1个数中若干个相加得到。 #include<cstdio> #include<cstdlib> #include<iostream> #include<fstream> #incl…

Spring Boot Admin 报警提醒和登录验证功能实现!

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;Spring Boot Admin&#xff08;SBA&#xff09;是一个开源的社区项目&#xff0c;用于管理和监控 Spring Boot 应用程序&…

企业Shell面试题18:单词及字母去重排序案例

1、按单词出现频率降序排序&#xff01; 2、按字母出现频率降序排序&#xff01; the squid project provides a number of resources to assist users design,implement and support squid installations. Please browse the documentation and support sections for more inf…