Redis Realizes Friends' Attention | Dark Horse Comments

Table of contents

1. Follow and unfollow

2. Common concern          

3. Pay attention to push (feed flow)

1. The scheme of Timeline mode

pull mode

push mode

push-pull mode

Summarize

2. Push mode to realize attention and push

need

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

  1. Follow and unsubscribe interface
  2. 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);
}

Tags: Java Spring Redis Spring Boot

Posted by ahundiak on Thu, 26 Jan 2023 21:07:24 +0530