Redis实现朋友圈,微博等Feed流功能,实现Feed流微服务(代码实现)

添加 Feed 信息

FeedsController

    /**
     * 添加 Feed
     *
     * @param feeds
     * @param access_token
     * @return
     */
    @PostMapping
    public ResultInfo<String> create(@RequestBody Feeds feeds, String access_token) {
        feedsService.create(feeds, access_token);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), "添加成功");
    }

FeedsService

    /**
     * 添加 Feed
     *
     * @param feeds
     * @param accessToken
     */
    @Transactional(rollbackFor = Exception.class)
    public void create(Feeds feeds, String accessToken) {
        // 校验 Feed 内容不能为空不能太长
        AssertUtil.isNotEmpty(feeds.getContent(), "请输入内容");
        AssertUtil.isTrue(feeds.getContent().length() > 255, "输入内容太多请重新输入");
        // 获取登录用户信息
        SignInUserInfo userInfo = loadSignInUserInfo(accessToken);
        // Feed 关联用户信息
        feeds.setFkUserId(userInfo.getId());
        // 添加 Feed
        int count = feedsMapper.save(feeds);
        AssertUtil.isTrue(count == 0, "添加失败");
        // 推送到粉丝的列表中 -- 后续这里应该采用异步消息队列解决性能问题
        // 先获取粉丝 id 集合
        List<Integer> followers = findFollowers(userInfo.getId());
        // 推送 Feed
        long now = System.currentTimeMillis();
        followers.forEach(follower -> {
            String key = RedisKeyConstant.following_feeds.getKey() + follower;
            redisTemplate.opsForZSet().add(key, feeds.getId(), now);
        });
    }

    /**
     * 获取粉丝 id 集合
     *
     * @param userId
     * @return
     */
    private List<Integer> findFollowers(Integer userId) {
        String url = followServerName + "followers/" + userId;
        ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class);
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
        }
        List<Integer> followers = (List<Integer>) resultInfo.getData();
        return followers;
    }

FeedsMapper

    /**
     * 添加 Feed
     * @param feeds 
     * @return
     */
    @Insert("insert into t_feeds (content, fk_user_id, praise_amount, " +
            " comment_amount, fk_restaurant_id, create_date, update_date, is_valid) " +
            " values (#{content}, #{fkUserId}, #{praiseAmount}, #{commentAmount}, #{fkRestaurantId}, " +
            " now(), now(), 1)")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int save(Feeds feeds);

ms-follow 服务新增获取粉丝列表

FollowController

    /**
     * 获取粉丝列表
     *
     * @param userId
     * @return
     */
    @GetMapping("followers/{userId}")
    public ResultInfo findFollowers(@PathVariable Integer userId) {
        return ResultInfoUtil.buildSuccess(request.getServletPath(),
                followService.findFollowers(userId));
    }

FollowService

    /**
     * 获取粉丝列表
     *
     * @param userId
     * @return
     */
    public Set<Integer> findFollowers(Integer userId) {
        AssertUtil.isNotNull(userId, "请选择要查看的用户");
        Set<Integer> followers = redisTemplate.opsForSet()
                .members(RedisKeyConstant.followers.getKey() + userId);
        return followers;
    }

ms-gateway 服务配置网关路由

spring:
  application:
    name: ms-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启配置注册中心进行路由功能
          lower-case-service-id: true # 将服务名称转小写
      routes:
          # Feed服务路由             
        - id: ms-feeds
          uri: lb://ms-feeds
          predicates:
            - Path=/feeds/**
          filters:
            - StripPrefix=1

启动项目测试

  1. 先让id等于10、9、8的用户关注id等于7的用户。
  2. id等于7的用户登录后发布一条动态。

让 id=10 的用户关注 id=7 的用户
在这里插入图片描述
在这里插入图片描述
让 id=9 的用户关注 id=7 的用户
在这里插入图片描述
让 id=8 的用户关注 id=7 的用户
image.png
id=7 的用户登录系统并发送一条动态
http://localhost/feeds?access_token=48781f97-1c3a-4737-ae55-984c0944649e
image.png
查看数据库 feeds 信息
在这里插入图片描述
查看 redis 中粉丝的 feeds 信息
在这里插入图片描述
可以看到用户id为8、9、10的用户都收到了这条Feed。

删除 Feed 信息

FeedsController

    /**
     * 删除 Feed
     *
     * @param id
     * @param access_token
     * @return
     */
    @DeleteMapping("{id}")
    public ResultInfo delete(@PathVariable Integer id, String access_token) {
        feedsService.delete(id, access_token);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), "删除成功");
    }

FeedsService

    /**
     * 删除 Feed
     *
     * @param id
     * @param accessToken
     */
    @Transactional(rollbackFor = Exception.class)
    public void delete(Integer id, String accessToken) {
        // 请选择要删除的 Feed
        AssertUtil.isTrue(id == null || id < 1, "请选择要删除的Feed");
        // 获取登录用户
        SignInUserInfo userInfo = loadSignInUserInfo(accessToken);
        // 获取 Feed 内容
        Feeds feeds = feedsMapper.findById(id);
        // 判断 Feed 是否已经被删除且只能删除自己的 Feed
        AssertUtil.isTrue(feeds == null, "该Feed已被删除");
        AssertUtil.isTrue(!feeds.getFkUserId().equals(userInfo.getId()),
                "只能删除自己的Feed");
        // 删除
        int count = feedsMapper.delete(id);
        if (count == 0) {
            return;
        }
        // 将内容从粉丝的集合中删除 -- 异步消息队列优化
        // 先获取我的粉丝
        List<Integer> followers = findFollowers(userInfo.getId());
        // 移除 Feed
        followers.forEach(follower -> {
            String key = RedisKeyConstant.following_feeds.getKey() + follower;
            redisTemplate.opsForZSet().remove(key, feeds.getId());
        });
    }

FeedsMapper

在这里插入图片描述

启动项目测试

数据库中的feeds

用户只能删除自己创建的Feed测试用id为6的用户删除id为14的Feed该Feed是id为7的用户创建的
在这里插入图片描述
在这里插入图片描述

用id为7的用户登陆后逻辑删除id=14的feeds
image.png
删除后再次删除
在这里插入图片描述
查看数据库中的feeds已经逻辑删除
在这里插入图片描述
查看redis相关Feed也不存在了
image.png

关注/取关时处理用户 Feed

当A用户关注B用户时那么要实时的将B的所有Feed推送到A用户的Feed集合中同样如果A用户取关B用户那么要将B用户所有的Feed从A用户的Feed集合中移除。

FeedsController

    /**
     * 变更 Feed
     *
     * @return
     */
    @PostMapping("updateFollowingFeeds/{followingDinerId}")
    public ResultInfo addFollowingFeeds(@PathVariable Integer followingDinerId,
                                        String access_token, @RequestParam int type) {
        feedsService.addFollowingFeed(followingDinerId, access_token, type);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), "操作成功");
    }

FeedsService

    /**
     * 变更 Feed
     *
     * @param followinguserId 关注的好友 ID
     * @param accessToken      登录用户token
     * @param type             1 关注 0 取关
     */
    @Transactional(rollbackFor = Exception.class)
    public void addFollowingFeed(Integer followinguserId, String accessToken, int type) {
        // 请选择关注的好友
        AssertUtil.isTrue(followinguserId == null || followinguserId < 1,
                "请选择关注的好友");
        // 获取登录用户信息
        SignInUserInfo userInfo = loadSignInUserInfo(accessToken);
        // 获取关注/取关的用户的所有 Feed
        List<Feeds> feedsList = feedsMapper.findByUserId(followinguserId);
        String key = RedisKeyConstant.following_feeds.getKey() + userInfo.getId();
        if (type == 0) {
            // 取关
            List<Integer> feedIds = feedsList.stream()
                    .map(feed -> feed.getId())
                    .collect(Collectors.toList());
            redisTemplate.opsForZSet().remove(key, feedIds.toArray(new Integer[]{}));
        } else {
            // 关注
            Set<ZSetOperations.TypedTuple> typedTuples =
                    feedsList.stream()
                            .map(feed -> new DefaultTypedTuple<>(feed.getId(), (double) feed.getUpdateDate().getTime()))
                            .collect(Collectors.toSet());
            redisTemplate.opsForZSet().add(key, typedTuples);
        }
    }

FeedsMapper

/**
* 根据用户 ID 查询 Feed
* @param userId
* @return
*/
@Select("select id, content, update_date from t_feeds " +
        " where fk_user_id = #{userId} and is_valid = 1")
    List<Feeds> findByUserId(@Param("userId") Integer userId);

ms-follow 服务关注取关时变更 Feed

添加调用ms-feeds服务的请求地址项目路径
在这里插入图片描述

FollowService新增关注/取关时Feed逻辑

    /**
     * 发送请求添加或者移除关注人的Feed列表
     *
     * @param followUserId 关注好友的ID
     * @param accessToken   当前登录用户token
     * @param type          0=取关 1=关注
     */
    private void sendSaveOrRemoveFeed(Integer followUserId, String accessToken, int type) {
        String feedsUpdateUrl = feedsServerName + "updateFollowingFeeds/"
                + followUserId + "?access_token=" + accessToken;
        // 构建请求头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        // 构建请求体请求参数
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        body.add("type", type);
        HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
        restTemplate.postForEntity(feedsUpdateUrl, entity, ResultInfo.class);
    }

启动项目测试

用户8910都关注了用户7

在这里插入图片描述
那么用户7推送一条feeds(朋友圈) 的时候他的粉丝用户89,10应该都可以看到测试用户7发送feed
在这里插入图片描述
查看数据库和redis
image.png
在这里插入图片描述

用户10取消关注用户7

用户10的feeds集合中存储了关注用户的feeds :
image.png
让用户10取消关注用户7
在这里插入图片描述
用户7的所有feeds(朋友圈) 应该从用户10的feeds集合中移除
在这里插入图片描述

只剩下用户8、9相关的。

用户11关注用户7

image.png

在这里插入图片描述
用户7的所有feeds(朋友圈) 应该都添加到用户11的feeds集合中
在这里插入图片描述

分页获取关注的 Feed 数据

当前数据库用户7发布了

构建返回的FeedsVO

/**
 *
 * Feed显示信息
 * @author zjq
 */
@Getter
@Setter
@ApiModel(description = "Feed显示信息")
public class FeedsVO implements Serializable {

    @ApiModelProperty("主键")
    private Integer id;
    @ApiModelProperty("内容")
    private String content;
    @ApiModelProperty("点赞数")
    private int praiseAmount;
    @ApiModelProperty("评论数")
    private int commentAmount;
    @ApiModelProperty("餐厅id")
    private Integer fkRestaurantId;
    @ApiModelProperty("用户ID")
    private Integer fkUserId;
    @ApiModelProperty("用户信息")
    private ShortUserInfo userInfo;
    @ApiModelProperty("显示时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
    public Date createDate;

}

FeedsController

/**
* 分页获取关注的 Feed 数据
*
* @param page
* @param access_token
* @return
*/
@GetMapping("{page}")
    public ResultInfo selectForPage(@PathVariable Integer page, String access_token) {
    List<FeedsVO> feedsVOS = feedsService.selectForPage(page, access_token);
return ResultInfoUtil.buildSuccess(request.getServletPath(), feedsVOS);
}

FeedsService

登录用户每次发送朋友圈都会向粉丝的feeds集合中推送这条朋友圈那么当粉丝就可以获取关注的人的所有feeds。
比如用户8,9,11关注了用户7那么用户7发的5条朋友圈用户8,9,11都能看到,用户8同时还跟用户6是好友那么用户8可以同时看到用户7和用户6发送的3条朋友圈。
在这里插入图片描述

在这里插入图片描述
实现逻辑如下

  • 获取登录用户信息
  • 构建分页查询的参数startend
  • 从Redis的sorted sets中按照score的降序进行读取Feed的id
  • 从数据库中获取Feed的信息
  • 构建Feed关联的用户信息不是循环逐条读取而是批量获取
    /**
     * 根据时间由近至远每次查询 6 条 Feed
     *
     * @param page
     * @param accessToken
     * @return
     */
    public List<FeedsVO> selectForPage(Integer page, String accessToken) {
        if (page == null) {
            page = 1;
        }
        // 获取登录用户
        SignInUserInfo userInfo = loadSignInUserInfo(accessToken);
        // 我关注的好友的 Feedkey
        String key = RedisKeyConstant.following_feeds.getKey() + userInfo.getId();
        // SortedSet 的 ZREVRANGE 命令是闭区间
        long start = (page - 1) * ApiConstant.PAGE_SIZE;
        long end = page * ApiConstant.PAGE_SIZE - 1;
        Set<Integer> feedIds = redisTemplate.opsForZSet().reverseRange(key, start, end);
        if (feedIds == null || feedIds.isEmpty()) {
            return Lists.newArrayList();
        }
        // 根据多主键查询 Feed
        List<Feeds> feeds = feedsMapper.findFeedsByIds(feedIds);
        // 初始化关注好友 ID 集合
        List<Integer> followinguserIds = new ArrayList<>();
        // 添加用户 ID 至集合顺带将 Feeds 转为 Vo 对象
        List<FeedsVO> feedsVOS = feeds.stream().map(feed -> {
            FeedsVO feedsVO = new FeedsVO();
            BeanUtil.copyProperties(feed, feedsVO);
            // 添加用户 ID
            followinguserIds.add(feed.getFkUserId());
            return feedsVO;
        }).collect(Collectors.toList());
        // 远程调用获取 Feed 中用户信息
        ResultInfo resultInfo = restTemplate.getForObject(usersServerName + "findByIds?access_token=${accessToken}&ids={ids}",
                ResultInfo.class, accessToken, followinguserIds);
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
        }
        List<LinkedHashMap> userInfoMaps = (ArrayList) resultInfo.getData();
        // 构建一个 key 为用户 IDvalue 为 ShortuserInfo 的 Map
        Map<Integer, ShortUserInfo> userInfos = userInfoMaps.stream()
                .collect(Collectors.toMap(
                        // key
                        diner -> (Integer) diner.get("id"),
                        // value
                        diner -> BeanUtil.fillBeanWithMap(diner, new ShortUserInfo(), true)
                ));
        // 循环 VO 集合根据用户 ID 从 Map 中获取用户信息并设置至 VO 对象
        feedsVOS.forEach(feedsVO -> {
            feedsVO.setUserInfo(userInfos.get(feedsVO.getFkUserId()));
        });
        return feedsVOS;
    }

FeedsMapper

    /**
     * 根据多主键查询 Feed
     * @param feedIds
     * @return
     */
    @Select("<script> " +
            " select id, content, fk_user_id, praise_amount, " +
            " comment_amount, fk_restaurant_id, create_date, update_date, is_valid " +
            " from t_feeds where is_valid = 1 and id in " +
            " <foreach item=\"id\" collection=\"feedIds\" open=\"(\" separator=\",\" close=\")\">" +
            "   #{id}" +
            " </foreach> order by id desc" +
            " </script>")
    List<Feeds> findFeedsByIds(@Param("feedIds") Set<Integer> feedIds);

启动项目测试

查询用户8关注好友的feeds列表
用户8同时和用户7、用户6是好友那么用户8可以同时看到用户7的5条朋友圈和用户6发送的3条朋友圈。
在这里插入图片描述
调用分页查询接口查询如下
http://localhost/feeds/1?access_token=1d7eb176-2454-4fd4-96d2-9c2d27c0ace6
image.png
可以看到顺序是按照最新的在最上面。

本文内容到此结束了
如有收获欢迎点赞👍收藏💖关注✔️您的鼓励是我最大的动力。
如有错误❌疑问💬欢迎各位指出。
主页共饮一杯无的博客汇总👨‍💻

保持热爱奔赴下一场山海。🏃🏃🏃

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: redis