Table of contents
3. Pay attention to push (feed flow)
1. The scheme of Timeline mode
2. Push mode to realize attention and push
feed stream pagination problem
Scrolling pagination of the feed stream
Implement push to fans' inboxes
Scrolling pagination to receive ideas
Implement scrolling pagination query
1. Follow and unfollow
When loading, it will first send a request to see if it is followed, to display whether it is a follow button or a cancel button
When we click to follow or cancel, we will send a request to operate
database table structure
Follow table (primary key, user id, follow user id)
need
- Follow and unsubscribe interface
- Determine whether to pay attention to the interface
/** * Follow users * @param id * @param isFollow * @return */ @PutMapping("/{id}/{isFollow}") public Result follow(@PathVariable("id") Long id, @PathVariable("isFollow") Boolean isFollow){ return followService.follow(id,isFollow); } /** * Determine whether to follow a specified user * @param id * @return */ @GetMapping("/or/not/{id}") public Result isFollow(@PathVariable("id") Long id){ return followService.isFollow(id); }
/** * Follow users * @param id * @param isFollow * @return */ @Override public Result follow(Long id, Boolean isFollow) { //Get the current user id Long userId = UserHolder.getUser().getId(); //Determine whether to follow the operation or cancel the operation if(BooleanUtil.isTrue(isFollow)){ //Focus on operation Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(id); save(follow); }else{ //Cancel operation remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",id)); } return Result.ok(); } /** * Determine whether to follow a specified user * @param id * @return */ @Override public Result isFollow(Long id) { //Get the current user id Long userId = UserHolder.getUser().getId(); Integer count = query().eq("user_id", userId).eq("follow_user_id", id).count(); if(count>0){ return Result.ok(true); } return Result.ok(false); }
2. Common concern
Requirement: Use the appropriate data structure in redis to realize the common attention function, and display the mutual friends of the current user and the blogger on the blogger's personal page
It can be realized by taking the intersection of the set structure in redis
First increase the deposit in redis in the follow and cancel
/** * Follow users * @param id * @param isFollow * @return */ @Override public Result follow(Long id, Boolean isFollow) { //Get the current user id Long userId = UserHolder.getUser().getId(); String key = "follow:" + userId; //Determine whether to follow the operation or cancel the operation if(BooleanUtil.isTrue(isFollow)){ //Focus on operation Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(id); boolean success = save(follow); if(success){ //Insert into the set collection stringRedisTemplate.opsForSet().add(key,id.toString()); } }else{ //Cancel operation boolean success = remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", id)); //remove from set if(success){ stringRedisTemplate.opsForSet().remove(key,id.toString()); } } return Result.ok(); }
Then you can start to write and view the mutual friend interface
/** * Determine whether to follow a specified user * @param id * @return */ @GetMapping("common/{id}") public Result followCommons(@PathVariable("id") Long id){ return followService.followCommons(id); }
/** * Common concern * @param id * @return */ @Override public Result followCommons(Long id) { Long userId = UserHolder.getUser().getId(); //current user's key String key1 = "follow:" + userId; //Specify the user's key String key2 = "follow:" + id; //Determine the intersection of two users Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2); if(intersect==null||intersect.isEmpty()){ //Indicates no common concern return Result.ok(); } //If there is a common concern, get the information of these users List<Long> userIds = intersect.stream().map(Long::valueOf).collect(Collectors.toList()); List<UserDTO> userDTOS = userService.listByIds(userIds).stream().map(item -> (BeanUtil.copyProperties(item, UserDTO.class))).collect(Collectors.toList()); return Result.ok(userDTOS); }
3. Pay attention to push (feed flow)
Follow push is also called fedd flow, literally translated as feeding. Continuously provide users with an "immersive" experience, and obtain new information through infinite pull-down refresh. feed mode, the content matches the user.
There are two common patterns for Feed stream products:
Timeline: No content screening, simply sorted by content release time, often used for friends or followers. For example circle of friends
- Advantages: comprehensive information, there will be no missing. And it is relatively simple to implement
- Disadvantages: There is a lot of information noise, users may not be interested, and the efficiency of content acquisition is low
Intelligent sorting: Use intelligent algorithms to block out content that violates regulations and is not of interest to users. Push information that users are interested in to attract users
- Advantages: Feed the information that users are interested in, the user viscosity is very high, and it is easy to become addicted
- Disadvantage: If the algorithm is not accurate, it may be counterproductive
In this example, the Feed stream is based on the friends you follow, so the Timeline mode is used.
1. The scheme of Timeline mode
Implementations of this model are
- pull mode
- push mode
- push-pull combination
pull mode
Advantages: Save memory messages, only need to save one copy, save the sender's outbox, and just pull it when you want to read it
Disadvantages: Every time you read it, you have to pull it, which takes a long time
push mode
Pros: low latency
Disadvantages: It takes up too much space, and a message needs to be saved many times
push-pull mode
Push-pull combines sub-users. For example, many fans of big v adopt the push mode and have their own outbox, allowing users to pull after they go online. If ordinary people post, they will use the push mode to push to each user, because there are not many fans and directly push to everyone with low delay. Fans are also divided into active fans and ordinary fans. Active fans use the push mode to have the inbox of the host, because they must read it every day, while ordinary fans use the pull mode, which is active online and then pulled. Zombie fans will not pull directly. Just save space.
Summarize
Since our review site has a relatively small number of users, we use the push mode (no problem if it is less than 10 million).
2. Push mode to realize attention and push
need
(1) Modify the business of adding shop-exploring notes, and push them to fans' inboxes while saving the blog to the database
(2) The inbox can be sorted according to time, which must be realized with the data structure of redis
(3) When querying inbox data, paged query can be realized
To perform paging query, what data type do we use to store in redis, is it list or zset?
feed stream pagination problem
If we add new content 11 at this time when we are querying by page, and when we query the next page, 6 will appear repeatedly. In order to solve this problem, we must use scrolling paging
Scrolling pagination of the feed stream
Scrolling pagination is to remember the last id every time, which is convenient for the next query. This lastid method is used to remember, and it does not depend on the corner mark, so we will not be affected by the corner mark. So we can't use list to store data, because it depends on the corner mark, and zset can query according to the range of score values. We sort by time, each time remembering the smallest last time, and then starting from the smaller one.
Implement push to fans' inboxes
Modify the business of adding new store notes, and push them to fans' inboxes while saving the blog to the database
@Override public Result saveBlog(Blog blog) { // 1. Get the logged in user UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); // 2. Save the store exploration notes boolean isSuccess = save(blog); if(!isSuccess){ return Result.fail("Failed to add note!"); } // 3. Query all followers of the note author select * from tb_follow where follow_user_id = ? List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list(); // 4. Push the note id to all fans for (Follow follow : follows) { // 4.1. Obtain fan id Long userId = follow.getUserId(); // 4.2. Push String key = FEED_KEY + userId; stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis()); } // 5. return id return Result.ok(blog.getId()); }
Scrolling pagination to receive ideas
The first query is the range of scores (time) from 1000 (large number) to 0 (minimum), and then limit the query to 3 (number of pages), the offset is 0, and then the end of the record (the last min)
From the last minimum value to 0 every time in the future, limit to check 3, the offset is 1 (because the recorded value is not counted), and then record the end value.
But there is a situation, if there is the same time and the same score, such as two 6 points, and the previous page has been displayed, our next page will end with the first 6 points, and the second 6 points may be It will appear, so our offset cannot be fixed at 1. It depends on how many numbers are the same as the end. If it is two, it must be 2, and if it is three, it must be 3.
Scrolling pagination query parameters:
- Maximum: current timestamp | minimum timestamp of last query
- Minimum value: 0
- Offset: 0 | The number of repetitions of the last value
- Limit number: the number displayed on one page
Implement scrolling pagination query
The front-end needs to transmit two pieces of data, namely lastId and offset. If it is the first query, these two values are fixed and will be specified by the front-end. lastId is the timestamp when the query was initiated, and offset is zero. After the backend queries the paging information, it needs to return three pieces of data. The first piece of data is naturally the paging information, the second piece of information is the timestamp of the last piece of data in the paging query data, and the third piece of information is the offset. We need to After paging query, calculate how many pieces of information have the same timestamp as the last one, and return them as offsets. After the front-end gets the last two parameters, they will be saved in the lastId and offset of the front-end respectively, and these two data will be accessed as request parameters in the next pagination query, and then the above process will be cycled continuously, so that it will be realized Paging query.
Define the return value entity class
@Data public class ScrollResult { private List<?> list; private Long minTime; private Integer offset; }
Controller
@GetMapping("/of/follow") public Result queryBlogOfFollow( @RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){ return blogService.queryBlogOfFollow(max, offset); }
BlogServiceImpl
@Override public Result queryBlogOfFollow(Long max, Integer offset) { //get current user Long userId = UserHolder.getUser().getId(); //Assembly key String key = RedisConstants.FEED_KEY + userId; //Query the inbox by page, query two items at a time ZREVRANGEBYSCORE key Max Min LIMIT offset count Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2); //Return directly if the inbox is empty if (typedTuples == null || typedTuples.isEmpty()) { return Result.ok(); } //Obtain the note id, offset and minimum time from the above data ArrayList<Long> ids = new ArrayList<>(); long minTime = 0; //Because the offset here is the offset to be passed to the front end next time, so the initial value is set to 1 int os = 1; for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) { //add blog id ids.add(Long.valueOf(typedTuple.getValue())); //get timestamp long score = typedTuple.getScore().longValue(); //Since the data is sorted in reverse order of timestamp, the last value assigned is the minimum time if (minTime == score) { //If there are two data timestamps equal, then the offset starts counting os++; } else { //If the timestamp of the current data is not equal to the minimum timestamp that has been recorded, it means that the current time is less than the minimum timestamp that has been recorded, and it is assigned to minTime minTime = score; //offset reset os = 1; } } //It is necessary to consider that the number of messages with equal timestamps is greater than 2. At this time, the offset needs to be added to the offset of the previous page query. os = minTime == max ? os : os + offset; //Query blog by id String idStr = StrUtil.join(",", ids); //You need to manually specify the order when querying List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list(); //It is also necessary to query the relevant information of the blogger. In the comparison video here, one query is used instead of multiple queries to improve efficiency. List<Long> blogUserIds = blogs.stream().map(blog -> blog.getUserId()).collect(Collectors.toList()); String blogUserIdStr = StrUtil.join(",", blogUserIds); HashMap<Long, User> userHashMap = new HashMap<>(); userService.query().in("id", blogUserIds).last("ORDER BY FIELD(id," + blogUserIdStr + ")").list(). stream().forEach(user -> { userHashMap.put(user.getId(), user); }); //Encapsulate data for blog Iterator<Blog> blogIterator = blogs.iterator(); while (blogIterator.hasNext()) { Blog blog = blogIterator.next(); User user = userHashMap.get(blog.getUserId()); blog.setName(user.getNickName()); blog.setIcon(user.getIcon()); blog.setIsLike(isLikeBlog(blog.getId())); } //return packaged data ScrollResult scrollResult = new ScrollResult(); scrollResult.setList(blogs); scrollResult.setMinTime(minTime); scrollResult.setOffset(os); return Result.ok(scrollResult); }