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问答网站项目视频

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

  1. Spring Boot 项目设置网站图标

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 作者 | 二师兄 来源 | 公众号「程序新视界」 正常情 ...

  2. apache目录 vscode_VsCode搭建Java开发环境(Spring Boot项目创建、运行、调试)

    VsCode搭建Java开发环境(Spring Boot项目创建.运行.调试) 安装如下两个主要扩展即可,这两个扩展已关联java项目开发主要使用的maven.springboot等所需要的扩展. 开 ...

  3. 电商生鲜网站开发(一)——Spring Boot项目开发准备

    本系列内容完成Spring Boot框架的电商生鲜网站开发的完整案例,前后端分离开发的案例,先开发后端接口后开发前端,最后部署等待. Spring Boot项目开发准备 文章目录 Spring Boo ...

  4. eclipse创建springboot项目_创建一个 Spring Boot 项目,你会几种方法?

    我最早是 2016 年底开始写 Spring Boot 相关的博客,当时使用的版本还是 1.4.x ,文章发表在 CSDN 上,阅读量最大的一篇有 42W+,如下图: 2017 年由于种种原因,就没有 ...

  5. maven web项目导入sts_Spring Boot2 系列教程(二)创建 Spring Boot 项目的三种方式

    我最早是 2016 年底开始写 Spring Boot 相关的博客,当时使用的版本还是 1.4.x ,文章发表在 CSDN 上,阅读量最大的一篇有 43W+,如下图: 2017 年由于种种原因,就没有 ...

  6. eclipse创建springboot项目_Spring Initializer+IntelliJ IDEA创建Spring Boot项目(图文)

    在本文中,将介绍如何使用Spring Initializer快速创建一个简单的Spring Boot项目.Spring Initializer是由Spring团队开发的一个优秀工具,通过使用它,你可以 ...

  7. java servlet 部署到tomcat_如何把spring boot项目部署到tomcat容器中

    把spring-boot项目按照平常的web项目一样发布到tomcat容器下 一.修改打包形式 在pom.xml里设置 war 二.移除嵌入式tomcat插件 在pom.xml里找到spring-bo ...

  8. 《SpringCloud超级入门》Spring Boot项目搭建步骤(超详细)《六》

    目录 编写第一个 REST 接口 读取配置文件 profiles 多环境配置 热部署 actuator 监控 自定义 actuator 端点 统一异常处理 异步执行 随机端口 编译打包 在 Sprin ...

  9. IDEA创建第一个Spring Boot项目

    Spring Boot官方参考指南 Spring Boot慨念请自行百度查找,这里就不多说了 一. 创建项目 File -> New -> Project 选择 Spring Initia ...

最新文章

  1. 2018年计算机职称考试冲刺,2018年中级会计职称考试考前30天冲刺计划和学习方法...
  2. RedHat7.4最小化安装yum源不可用问题解决
  3. 活动 | 优步首席产品官Jeff Holden携手百度总裁张亚勤,告诉你Uber高速增长的秘密...
  4. springmvc02
  5. hdata datax交流总结
  6. 中国甲鱼养殖行业发展现状分析,浙江省产量最高「图」
  7. word修改正文样式影响标题样式
  8. 图解:卷帘快门(Rolling_shutter)与全局快门(global_shutter)的区别
  9. 创新洞察|为什么中国企业更擅长用户导向创新
  10. 【Java前端】CSS(一)[字体,文本属性,调试工具]
  11. 飞马D200S无人机与机载激光雷达在大比例尺DEM建设中的应用
  12. 征文分享|OceanBase 对分布式事务的支持能力评测与分析
  13. android重力传感器横竖反,Android手机在哪里设置重力传感器(转向屏幕)?
  14. jquery实现HTML复选框变单选框
  15. 系统硬盘分区模式之GPT
  16. 孤独和寂寞也是一种美
  17. 中国各大论坛链接地址(部分需要邀请码,稍感遗憾)!
  18. 连接跟踪TCP协议状态转换
  19. 微信小程序制作科学计算器(控制台接受显示数据)
  20. 2020C#程序设计及应用教程复习总结

热门文章

  1. JDK源码解析之 java.lang.Integer
  2. 互联网的发展背离了其初衷吗?
  3. 【Python爬虫学习笔记12】Ajax数据爬取简介
  4. 马凯军201771010116《面向对象程序设计(java)》第二周学习总结
  5. Quartz业务类无法注入Spring对象问题
  6. Reactjs 踏坑指南1: 一些概念
  7. 【js】JavaScript parser实现浅析
  8. Swift使用CoreLocation,你必须要看这一篇
  9. bitcount java_java-Long.bitCount()如何找到设置的位数?
  10. u8转完看不到菜单_web网页有错误,无法看到操作菜单-用友U8