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…

KMP POJ 2752 Seek the Name, Seek the Fame

题目传送门 1 /*2 题意&#xff1a;求出一个串的前缀与后缀相同的字串的长度3 KMP&#xff1a;nex[]就有这样的性质&#xff0c;倒过来输出就行了4 */5 /************************************************6 * Author :Running_Time7 * Created Time :2015-8-1…

c语言 函数的参数传递示例_C ++中带有示例的nearint()函数

c语言 函数的参数传递示例C 附近的int()函数 (C nearbyint() function) nearbyint() function is a library function of cmath header, it is used to round the given value to an integral value based on the specified direction by fegetround() function. It accepts a …

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 应用程序。应…

适用于各种列表操作的Python程序

Here, we are implementing a python program for various list operations, following operations are being performed in the list, 在这里&#xff0c;我们正在为各种列表操作实现python程序&#xff0c;正在列表中执行以下操作&#xff0c; Declaring an integer list 声…

一个障碍,就是一个超越自我的契机

一个障碍&#xff0c;就是一个新的已知条件&#xff0c;只要愿意&#xff0c;任何一个障碍&#xff0c;都会成为一个超越自我的契机。 有一天&#xff0c;素有森林之王之称的狮子&#xff0c;来到了 天神面前&#xff1a;"我很感谢你赐给我如此雄壮威武的体格、如此强大无…

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;那感觉只有两个字“真香…

页面访问的常见错误码解析

200 OK 一切正常301 Moved Permanently 客户请求的文档在其他地方&#xff0c;新的URL在Location头中给出&#xff0c;浏览器应该自动地访问新的URL。 302 Found 类似于301&#xff0c;但新的URL应该被视为临时性的替代&#xff0c;而不是永久性的。注意&#xff0c;在HTT…

aptitude_PHP Numbers Aptitude问题与解答

aptitudeThis section contains Aptitude Questions and Answers on PHP Numbers. 本节包含有关PHP数字的能力问题。 1) PHP supports automatic type conversion? YesNo Answer & Explanation Correct answer: 1Yes Yes, PHP supports automatic type conversion. 1)PHP…

SpringBoot + ShardingSphere 秒级分库分表!

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

JAVA基础之自定义容器实现

容器 容器主要是指Collection所包含的实现类&#xff0c;常用的有List、Map以及Set三种结构。本文主要介绍了几种常见的集合实现类&#xff0c;对它们进行自定义实现。 ArrayList&#xff1a;有序的容器列表&#xff0c;顺序存储着元素&#xff0c;可以使用下标进行索引&…

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字符串以空字符结尾的字符…

weakhashmap_Java WeakHashMap size()方法与示例

weakhashmapWeakHashMap类的size()方法 (WeakHashMap Class size() method) size() method is available in java.util package. size()方法在java.util包中可用。 size() method is used to get the number of key-value pairs that exist in this map. size()方法用于获取此映…

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

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

在项目中引入领域驱动设计的经验

Chris Patuzzo近期在一次演讲中介绍了领域驱动设计&#xff08;DDD&#xff09;的原则&#xff0c;并结合一个基于Ruby on Rails的真实项目进行讲解。在这次项目之前&#xff0c;Chris所在的团队为重新设计公司的主营网站所做的两个概念验证都因为可伸缩性方面的问题而失败了。…

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

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

treeset java_Java TreeSet iterator()方法与示例

treeset javaTreeSet类的iterator()方法 (TreeSet Class iterator() method) iterator() method is available in java.util package. iterator()方法在java.util包中可用。 iterator() method is used to iterate the elements of this TreeSet is ascending or increasing or…