微博新鲜事功能介绍:关注好友的动态,比如关注好友的点赞,发表的问题,关注了某个问题等信息,都是feed流的一部分。 在知乎中的feed流主要体现于:关注用户的评论行为,关注用户的关注问题行为。 feed流主要分为两种,推模式和拉模式,推模式主要是把新鲜事推送给关注该用户的粉丝,本例使用redis来存储某个用户接受的新鲜事id列表。这个信息流又称为timeline,根据用户的唯一key来存储。
拉模式主要是用户直接找寻自己所有关注的人,并且到数据库去查找这些关注对象的新鲜事,直接返回。 推模式主要适合粉丝较少的小用户,因为他们的粉丝量少,使用推模式产生的冗余副本也比较少,并且可以减少用户访问的压力。 拉模式主要适合大v,因为很多僵尸粉根本不需要推送信息,用推模式发给这些僵尸粉就是浪费资源,所以让用户通过拉模式请求,只需要一个数据副本即可。同时如果是热点信息,这些信息也可以放在缓存,让用户首先拉取这些信息,提高查询效率。
开发
EventHandlerFeedDAO/Service/ModelRedis队列存储①Feed
public class Feed { private int id;//新鲜事id private int type;//新鲜事类型 private int userId; private Date createdDate; private String data;//json格式 private JSONObject dataJSON = null; public 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); } public String get(String key) { return dataJSON == null ? null : dataJSON.getString(key); } }②FeedDAO
注解实现简单sql 插入 xml实现动态sql 查询和userId相关的新鲜事
@Mapper public interface FeedDAO { String TABLE_NAME = " feed "; String INSERT_FIELDS = " user_id, data, created_date, type "; String SELECT_FIELDS = " id, " + INSERT_FIELDS; @Insert({"insert into ", TABLE_NAME, "(", INSERT_FIELDS, ") values (#{userId},#{data},#{createdDate},#{type})"}) int addFeed(Feed feed);//插入一个feed @Select({"select ", SELECT_FIELDS, " from ", TABLE_NAME, " where id=#{id}"}) Feed getFeedById(int id);//推模式,拿到的是id,去取 //拉的模式 去取关注的人或者系统推荐的feed List<Feed> selectUserFeeds(@Param("maxId") int maxId,//增量的去更新 @Param("userIds") List<Integer> userIds,//若没登陆,这个这个变量不应该用起来 @Param("count") int count); }FeedDao.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.nowcoder.dao.FeedDAO"> <sql id="table">feed</sql> <sql id="selectFields">id, created_date,user_id, data, type </sql> <select id="selectUserFeeds" resultType="com.nowcoder.model.Feed"> SELECT <include refid="selectFields"/> FROM <include refid="table"/> WHERE id < #{maxId} <if test="userIds.size() != 0"> AND user_id in <foreach item="item" index="index" collection="userIds" open="(" separator="," close=")"> #{item} </foreach> </if> ORDER BY id DESC LIMIT #{count} </select> </mapper>③FeedService 查询list对应的新鲜事
@Service public class FeedService { @Autowired FeedDAO feedDAO; public List<Feed> getUserFeeds(int maxId, List<Integer> userIds, int count) {//拉模式 return feedDAO.selectUserFeeds(maxId, userIds, count); } public boolean addFeed(Feed feed) { feedDAO.addFeed(feed); return feed.getId() > 0; } public Feed getById(int id) {//推模式 return feedDAO.getFeedById(id); } }④FeedHandler
@Component public class FeedHandler implements EventHandler { @Autowired FollowService followService; @Autowired UserService userService; @Autowired FeedService feedService; @Autowired JedisAdapter jedisAdapter; @Autowired QuestionService questionService; private String buildFeedData(EventModel model) { Map<String, String> map = new HashMap<String ,String>(); // 触发用户是通用的 User actor = userService.getUser(model.getActorId()); if (actor == null) { return null; } map.put("userId", String.valueOf(actor.getId())); map.put("userHead", actor.getHeadUrl()); map.put("userName", actor.getName()); if (model.getType() == EventType.COMMENT || (model.getType() == EventType.FOLLOW && model.getEntityType() == EntityType.ENTITY_QUESTION)) { Question question = questionService.getById(model.getEntityId()); if (question == null) { return null; } map.put("questionId", String.valueOf(question.getId())); map.put("questionTitle", question.getTitle()); return JSONObject.toJSONString(map); } return null; } @Override public void doHandle(EventModel model) {//当发现有人评论时被调用 // 为了测试,把model的userId随机一下 Random r = new Random(); model.setActorId(1+r.nextInt(10)); // 构造一个新鲜事 Feed feed = new Feed(); feed.setCreatedDate(new Date()); feed.setType(model.getType().getValue()); feed.setUserId(model.getActorId()); feed.setData(buildFeedData(model)); if (feed.getData() == null) { // 不支持的feed return; } feedService.addFeed(feed); // 获得所有粉丝 List<Integer> followers = followService.getFollowers(EntityType.ENTITY_USER, model.getActorId(), Integer.MAX_VALUE); // 系统队列 followers.add(0); // 给所有粉丝推事件 for (int follower : followers) { String timelineKey = RedisKeyUtil.getTimelineKey(follower); jedisAdapter.lpush(timelineKey, String.valueOf(feed.getId())); // 限制最长长度,如果timelineKey的长度过大,就删除后面的新鲜事 } } @Override public List<EventType> getSupportEventTypes() { return Arrays.asList(new EventType[]{EventType.COMMENT, EventType.FOLLOW});//评论和关注都加一个事件出来 } }⑤FeedController
拉模式 feedHandler 当如评论,关注等Event事件发生时,生成一个feed并入数据库 getPullFeeds:拉模式 根据登录用户取出关注的人,得到id列表list 调用Service从数据库得到所有的新鲜事 用macro宏在模板根据type中渲染出不同样式(feeds.html中) 推模式 feedHandler 当把feed存入库时,还需要推入到粉丝的reids.<timelinekey,list>中 用户登录时根据自己的timelinekey直接从redis取出所有feed 注意当取消关注或者取消事件是需要同步redis
@Controller public class FeedController { private static final Logger logger = LoggerFactory.getLogger(FeedController.class); @Autowired FeedService feedService; @Autowired FollowService followService; @Autowired HostHolder hostHolder; @Autowired JedisAdapter jedisAdapter; @RequestMapping(path = {"/pushfeeds"}, method = {RequestMethod.GET, RequestMethod.POST}) private String getPushFeeds(Model model) { int localUserId = hostHolder.getUser() != null ? hostHolder.getUser().getId() : 0; List<String> feedIds = jedisAdapter.lrange(RedisKeyUtil.getTimelineKey(localUserId), 0, 10); List<Feed> feeds = new ArrayList<Feed>(); for (String feedId : feedIds) { Feed feed = feedService.getById(Integer.parseInt(feedId)); if (feed != null) { feeds.add(feed); } } model.addAttribute("feeds", feeds); return "feeds"; } @RequestMapping(path = {"/pullfeeds"}, method = {RequestMethod.GET, RequestMethod.POST})//拉的模式 private String getPullFeeds(Model model) { int localUserId = hostHolder.getUser() != null ?//本地用户 hostHolder.getUser().getId() : 0; List<Integer> followees = new ArrayList<>(); if (localUserId != 0) { // 关注的人 followees = followService.getFollowees(localUserId, EntityType.ENTITY_USER, Integer.MAX_VALUE); } List<Feed> feeds = feedService.getUserFeeds(Integer.MAX_VALUE, followees, 10); model.addAttribute("feeds", feeds);//去渲染 return "feeds"; } }使用feedhandler异步处理上述的两个事件,当事件发生时,根据事件实体进行重新包装,构造一个新鲜事,因为所有新鲜事的格式是一样的。需要包括:日期,新鲜事类型,发起者,新鲜事内容,然后把该数据存入数据库,以便用户使用pull模式拉出。 为了适配推送模式,此时也要把新鲜事放到该用户所有粉丝的timeline里,这样的话就同时实现了推和拉的操作了。
优化
多好友合并去重关联实体删除清理取消/新增关注实时更新分时段存储缓存和增量拉