日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

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

發布時間:2025/3/11 javascript 17 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Spring boot项目(问答网站)之timeline的推拉两种模式 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Timeline介紹

所謂timeline就是當用戶打開主頁看到的隨著時間軸發生的一系列時間的整合,主要包含:

  • 關注用戶的最新動態
  • 熱門推薦
  • 廣告推薦整合等等.

推、拉模式

  • 推模式: 當一個用戶關注了或者評論了一個問題或用戶,觸發事件,將會將這一動態廣播給該用戶所有的粉絲
  • 拉模式: 用戶由于某種行為觸發事件后,不會廣播給每一個粉絲,只有當粉絲主動查詢該用戶最近動態時,才從緩存中讀取,組建timeline內容。
  • 推拉結合:當一個用戶的粉絲數目過多時,推模式會對后臺造成很大壓力,浪費存貯空間,特別有些粉絲很有可能已經很久未登錄;同樣的,當一個用戶關注的人特別多,或者同時有很多用戶同時查詢同樣的動態時,對后臺讀取數據壓力特別大。于是,在大多數大規模網站采用一種推拉結合的模式,即是對活躍的用戶采用推模式,對不活躍用戶采用拉模式,這樣就緩解了后端的壓力,同時滿足了大多數用戶的需求。
  • 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的推拉两种模式的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。