Redian新闻
>
Redis实现微博好友功能微服务(关注,取关,共同关注)

Redis实现微博好友功能微服务(关注,取关,共同关注)

公众号新闻

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 
来源:zhanjq.blog.csdn.net/
article/details/128208047

需求分析

好友功能是目前社交场景的必备功能之一,一般好友相关的功能包含有:关注/取关、我(他)的关注、我(他)的粉丝、共同关注、我关注的人也关注他等这样一些功能。

类似于这样的功能我们如果采用数据库做的话只是单纯得到用户的一些粉丝或者关注列表的话是很简单也很容易实现, 但是如果我想要查出两个甚至多个用户共同关注了哪些人或者想要查询两个或者多个用户的共同粉丝的话就会很麻烦,效率也不会很高。

但是如果你用redis去做的话就会相当的简单而且效率很高。原因是redis自己本身带有专门针对于这种集合的交集、并集、差集的一些操作。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

设计思路

总体思路我们采用MySQL + Redis的方式结合完成。MySQL主要是保存落地数据,而利用Redis的Sets数据类型进行集合操作。

Sets拥有去重(我们不能多次关注同一用户)功能。一个用户我们存贮两个集合:一个是保存用户关注的人 另一个是保存关注用户的人。

  • SADD 添加成员: 命令格式: SADD key member [member …] ----- 关注
  • SREM 移除某个成员: 命令格式: SREM key member [member …] -------取关
  • SCARD 统计集合内的成员数: 命令格式: SCARD key -------关注/粉丝个数
  • SISMEMBER 判断是否是集合成员: 命令格式:SISMEMBER key member ---------判断是否关注(如果关注那么只可以点击取关)
  • SMEMBERS 查询集合内的成员: 命令格式: SMEMBERS key -------列表使用(关注列表和粉丝列表)
  • SINTER 查询集合的交集: 命令格式: SINTER key [key …] --------共同关注、我关注的人关注了他

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

数据库表设计

这个数据库表的结构比较简单,主要记录了用户id、用户关注的id和关注状态。

CREATE TABLE `t_follow` (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `user_id` int(11DEFAULT NULL COMMENT '当前登录用户的id',
  `follow_user_id` int(11DEFAULT NULL COMMENT '当前登录用户关注的用户的id',
  `is_valid` tinyint(1DEFAULT NULL COMMENT '关注状态,0-没有关注,1-关注了',
  `create_date` datetime DEFAULT NULL,
  `update_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='用户和用户关注表';

新建好友功能微服务

添加依赖和配置

pom依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>
        <artifactId>redis-seckill</artifactId>
        <groupId>com.zjq</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>ms-follow</artifactId>

    <dependencies>
        <!-- eureka client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- spring web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- spring data redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!-- commons 公共项目 -->
        <dependency>
            <groupId>com.zjq</groupId>
            <artifactId>commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!-- swagger -->
        <dependency>
            <groupId>com.battcn</groupId>
            <artifactId>swagger-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

</project>

springboot配置如下:

server:
  port: 7004 # 端口

spring:
  application:
    name: ms-follow # 应用名
  # 数据库
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/seckill?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
  # Redis
  redis:
    port: 6379
    host: localhost
    timeout: 3000
    password: 123456
    database: 2
  # Swagger
  swagger:
    base-package: com.zjq.follow
    title: 好用功能微服务API接口文档

# 配置 Eureka Server 注册中心
eureka:
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka/

service:
  name:
    ms-oauth-server: http://ms-oauth2-server/
    ms-diners-server: http://ms-users/

mybatis:
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰映射

logging:
  pattern:
    console: '%d{HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'

添加配置类

redis配置类:

package com.zjq.seckill.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * RedisTemplate配置类
 * @author zjq
 */

@Configuration
public class RedisTemplateConfiguration {

    /**
     * redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
     *
     * @param redisConnectionFactory
     * @return
     */

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置key和value的序列化规则
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
    

}

REST配置类:

关注/取关实现

业务逻辑

Mapper实现

Mapper比较简单主要是查询关注信息、添加关注信息、取关或者再次关注。

Service层实现

package com.zjq.seckill.service;

import cn.hutool.core.bean.BeanUtil;
import com.zjq.commons.constant.ApiConstant;
import com.zjq.commons.constant.RedisKeyConstant;
import com.zjq.commons.exception.ParameterException;
import com.zjq.commons.model.domain.ResultInfo;
import com.zjq.commons.model.pojo.Follow;
import com.zjq.commons.model.vo.SignInUserInfo;
import com.zjq.commons.utils.AssertUtil;
import com.zjq.commons.utils.ResultInfoUtil;
import com.zjq.seckill.mapper.FollowMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.LinkedHashMap;

/**
 * 关注/取关业务逻辑层
 * @author zjq
 */

@Service
public class FollowService {

    @Value("${service.name.ms-oauth-server}")
    private String oauthServerName;
    @Value("${service.name.ms-diners-server}")
    private String dinersServerName;
    @Resource
    private RestTemplate restTemplate;
    @Resource
    private FollowMapper followMapper;
    @Resource
    private RedisTemplate redisTemplate;


    /**
     * 关注/取关
     *
     * @param followUserId 关注的食客ID
     * @param isFollowed    是否关注 1=关注 0=取关
     * @param accessToken   登录用户token
     * @param path          访问地址
     * @return
     */

    public ResultInfo follow(Integer followUserId, int isFollowed,
                             String accessToken, String path)
 
{
        // 是否选择了关注对象
        AssertUtil.isTrue(followUserId == null || followUserId < 1,
                "请选择要关注的人");
        // 获取登录用户信息 (封装方法)
        SignInUserInfo dinerInfo = loadSignInDinerInfo(accessToken);
        // 获取当前登录用户与需要关注用户的关注信息
        Follow follow = followMapper.selectFollow(dinerInfo.getId(), followUserId);

        // 如果没有关注信息,且要进行关注操作 -- 添加关注
        if (follow == null && isFollowed == 1) {
            // 添加关注信息
            int count = followMapper.save(dinerInfo.getId(), followUserId);
            // 添加关注列表到 Redis
            if (count == 1) {
                addToRedisSet(dinerInfo.getId(), followUserId);
            }
            return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE,
                    "关注成功", path, "关注成功");
        }

        // 如果有关注信息,且目前处于关注状态,且要进行取关操作 -- 取关关注
        if (follow != null && follow.getIsValid() == 1 && isFollowed == 0) {
            // 取关
            int count = followMapper.update(follow.getId(), isFollowed);
            // 移除 Redis 关注列表
            if (count == 1) {
                removeFromRedisSet(dinerInfo.getId(), followUserId);
            }
            return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE,
                    "成功取关", path, "成功取关");
        }

        // 如果有关注信息,且目前处于取关状态,且要进行关注操作 -- 重新关注
        if (follow != null && follow.getIsValid() == 0 && isFollowed == 1) {
            // 重新关注
            int count = followMapper.update(follow.getId(), isFollowed);
            // 添加关注列表到 Redis
            if (count == 1) {
                addToRedisSet(dinerInfo.getId(), followUserId);
            }
            return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE,
                    "关注成功", path, "关注成功");
        }

        return ResultInfoUtil.buildSuccess(path, "操作成功");
    }

    /**
     * 添加关注列表到 Redis
     *
     * @param dinerId
     * @param followUserId
     */

    private void addToRedisSet(Integer dinerId, Integer followUserId) {
        redisTemplate.opsForSet().add(RedisKeyConstant.following.getKey() + dinerId, followUserId);
        redisTemplate.opsForSet().add(RedisKeyConstant.followers.getKey() + followUserId, dinerId);
    }

    /**
     * 移除 Redis 关注列表
     *
     * @param dinerId
     * @param followUserId
     */

    private void removeFromRedisSet(Integer dinerId, Integer followUserId) {
        redisTemplate.opsForSet().remove(RedisKeyConstant.following.getKey() + dinerId, followUserId);
        redisTemplate.opsForSet().remove(RedisKeyConstant.followers.getKey() + followUserId, dinerId);
    }

    /**
     * 获取登录用户信息
     *
     * @param accessToken
     * @return
     */

    private SignInUserInfo loadSignInDinerInfo(String accessToken) {
        // 必须登录
        AssertUtil.mustLogin(accessToken);
        String url = oauthServerName + "user/me?access_token={accessToken}";
        ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.classaccessToken);
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            throw new ParameterException(resultInfo.getMessage());
        }
        SignInUserInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
                new SignInUserInfo(), false);
        return dinerInfo;
    }

}

Controller实现

package com.zjq.seckill.controller;


import com.zjq.commons.model.domain.ResultInfo;
import com.zjq.seckill.service.FollowService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

/**
 * 关注/取关控制层
 * @author zjq
 */

@RestController
public class FollowController {

    @Resource
    private FollowService followService;
    @Resource
    private HttpServletRequest request;

    /**
     * 关注/取关
     *
     * @param followUserId 关注的用户ID
     * @param isFollowed    是否关注 1=关注 0=取消
     * @param access_token  登录用户token
     * @return
     */

    @PostMapping("/{followUserId}")
    public ResultInfo follow(@PathVariable Integer followUserId,
                             @RequestParam int isFollowed,
                             String access_token)
 
{
        ResultInfo resultInfo = followService.follow(followUserId,
                isFollowed, access_token, request.getServletPath());
        return resultInfo;
    }

}

网关配置路由规则

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

测试验证

依次启动,注册中心、网关、认证中心、好友功能微服务。

测试id为5的用户,关注id为1的用户。

查看redis可以看到有两个集合,一个粉丝集合,一个关注集合。

查看数据库,id为5的用户关注了id为1的用户

共同关注列表

从Redis中读取登录用户的关注列表与查看用户的关注列表,然后进行交集操作,获取共同关注的用户id

然后通过用户服务传入用户id数据获取用户基本信息

Controller添加方法

    /**
     * 共同关注列表
     *
     * @param userId
     * @param access_token
     * @return
     */

    @GetMapping("commons/{userId}")
    public ResultInfo findCommonsFriends(@PathVariable Integer userId,
                                         String access_token)
 
{
        return followService.findCommonsFriends(userId, access_token, request.getServletPath());
    }

Service添加方法

    /**
     * 共同关注列表
     *
     * @param userId
     * @param accessToken
     * @param path
     * @return
     */

    @Transactional(rollbackFor = Exception.class)
    public ResultInfo findCommonsFriends(Integer userIdString accessTokenString path
{
        // 是否选择了查看对象
        AssertUtil.isTrue(userId == null || userId < 1,
                "请选择要查看的人");
        // 获取登录用户信息
        SignInUserInfo userInfo = loadSignInuserInfo(accessToken);
        // 获取登录用户的关注信息
        String loginuserKey = RedisKeyConstant.following.getKey() + userInfo.getId();
        // 获取登录用户查看对象的关注信息
        String userKey = RedisKeyConstant.following.getKey() + userId;
        // 计算交集
        Set<Integer> userIds = redisTemplate.opsForSet().intersect(loginuserKey, userKey);
        // 没有
        if (userIds == null || userIds.isEmpty()) {
            return ResultInfoUtil.buildSuccess(path, new ArrayList<ShortUserInfo>());
        }
        // 调用食客服务根据 ids 查询食客信息
        ResultInfo resultInfo = restTemplate.getForObject(usersServerName + "findByIds?access_token={accessToken}&ids={ids}",
                ResultInfo.classaccessTokenStrUtil.join(",", userIds));
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            resultInfo.setPath(path);
            return resultInfo;
        }
        // 处理结果集
        List<LinkedHashMap> dinnerInfoMaps = (ArrayList) resultInfo.getData();
        List<ShortUserInfo> userInfos = dinnerInfoMaps.stream()
                .map(user -> BeanUtil.fillBeanWithMap(user, new ShortUserInfo(), true))
                .collect(Collectors.toList());

        return ResultInfoUtil.buildSuccess(path, userInfos);
    }

用户服务新增根据ids查询用户集合

Controller:

    /**
     * 根据 ids 查询用户信息
     *
     * @param ids
     * @return
     */

    @GetMapping("findByIds")
    public ResultInfo<List<ShortUserInfo>> findByIds(String ids) {
        List<ShortUserInfo> dinerInfos = userService.findByIds(ids);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), dinerInfos);
    }

Service:

    /**
     * 根据 ids 查询食客信息
     *
     * @param ids 主键 id,多个以逗号分隔,逗号之间不用空格
     * @return
     */

    public List<ShortUserInfo> findByIds(String ids) {
        AssertUtil.isNotEmpty(ids);
        String[] idArr = ids.split(",");
        List<ShortUserInfo> dinerInfos = usersMapper.findByIds(idArr);
        return dinerInfos;
    }

Mapper:

    /**
     * 根据 ID 集合查询多个食客信息
     * @param ids
     * @return
     */

    @Select("<script> " +
            " select id, nickname, avatar_url from t_diners " +
            " where is_valid = 1 and id in " +
            " <foreach item=\"id\" collection=\"ids\" open=\"(\" separator=\",\" close=\")\"> " +
            "   #{id} " +
            " </foreach> " +
            " </script>")
    List<ShortUserInfo> findByIds(@Param("ids") String[] ids);

上面测试已经让id5和7的用户关注了id为1的用户,我们继续让id5的用户关注id为3的用户,让id5、6、7的用户关注了id为2的用户。

redis和数据库信息如下:

测试验证

查询当前登录用户id为5和id为7的共同关注信息:

查询当前登录用户id为6和id为7的共同关注信息:

可以看出来5和7共同关注了1和2,6和7只共同关注了2,符合预期。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
离谱!赛琳娜纪录片讲病痛经历...却丝毫不提捐肾闺蜜?被对方直接取关,事后回应网友气炸!不再判定次密接,取消入境航班熔断机制,取消中风险,这份通知事关你我纽约林肯中心 Lincoln Center 欣赏交响乐演出实务总结:公募REITs实操手册(2022年版)新闻第34期|支付宝转账微信好友功能上线,支付行业互联互通的法律分析赛琳娜称霉霉是唯一好友,闺蜜愤怒取关:你的肾不痛吗...微服务虽已老生常谈,但生命力超出不少人想象 | 解读微服务的2022马蜂窝如何利用 APISIX 网关实现微服务架构升级WPP广州新办公园区正式启用;盛世长城广州赢得茶茶居创意传播业务(广告狂人日报)Bloom 美式早午餐金融互联网行业微服务架构必须要考虑的几件事 | ArchSummit为什么企业应该选择平台即服务(PaaS) | Linux 中国让无服务器微服务超越容器,开发工具初创公司Fermyon 推出 WebAssembly 云本号重要通知:请注意查看是否被取关今天的午餐和晚餐参会者调研结果出炉:微服务、集群调度、研发效能最受关注|QCon北京站闭幕Java 云原生微服务框架 Quarkus 入门实践Cyber Monday炸鸡甜甜圈超低价抄底, Popeyes实现买一送一自由所谓好人缘儿在美国276.监视器升级晶能微电子完成首轮融资,开发车规级功率半导体小米13系列定档;Redmi K60系列处理器曝光;华为WATCH Buds实物曝光一直不舍得取关的百万级公众号,个个经典!微博回应收购新浪传闻;特斯拉上海工厂停产;微信测试「划线」功能,看到金句可转发好友 | 极客早知道For Gen-Z, Entrepreneurship Represents a Ticket to FreedomGitHub 前 CTO:全面微服务是最大的架构错误!网友:这不是刚改完吗...“郭碧婷取关向佐疑似婚变??”网友笑疯:他俩离婚了向太跟谁啊…新年从“减”开始!用这四条标准,可取关2/3的公众号!Colenso BBDO任命新集团创意总监;伊莱克斯任命宏盟媒体集团负责媒体业务(广告狂人日报)英国近3年的学校公寓投诉有3000起与虫相关,其中有272起与Bed Bug相关...遇到了Bed Bug 改怎么办?功能饮料龙头获机构最多关注,9股评级上调,最新评级情况出炉基于Traefik的微服务网关二次开发!如何使用Kubernetes实现应用程序的弹性伸缩耐克比稿结果揭晓;OMD赢得Burberry全球媒介业务(广告狂人日报)闺蜜为赛琳娜捐肾,如今取关翻脸了!背后的真相细思极恐!
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。