职贝云数AI新零售门户
标题:
Redis完成微博好友功能微服务(关注,取关,共同关注)
[打印本页]
作者:
ymuUw7L16pYA
时间:
2023-1-4 00:18
标题:
Redis完成微博好友功能微服务(关注,取关,共同关注)
需求分析
好友功能是目前社交场景的必备功能之一,普通好友相关的功能包含有:关注/取关、我(他)的关注、我(他)的粉丝、共同关注、我关注的人也关注他等这样一些功能。
(, 下载次数: 1)
上传
点击文件名下载附件
相似于这样的功能我们假如采用数据库做的话只是单纯得到用户的一些粉丝或者关注列表的话是很简单也很容易完成, 但是假如我想要查出两个甚至少个用户共同关注了哪些人或者想要查询两个或者多个用户的共同粉丝的话就会很费事, 效率也不会很高。但是假如你用redis去做的话就会相当的简单而且效率很高。缘由是redis本人本身带有专门针对于这种集合的交集、并集、差集的一些操作。
(, 下载次数: 1)
上传
点击文件名下载附件
设计思绪
总体思绪我们采用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 …] --------共同关注、我关注的人关注了他
需求守旧正版IDEA的可以联络我,79元一年,正版授权,官网可查有效期,有需求的加我微信:poxiaozhiai6,备注:126。
数据库表设计
这个数据库表的结构比较简单,次要记录了用户id、用户关注的id和关注形态。
CREATE TABLE `t_follow` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL COMMENT '当前登录用户的id',
`follow_user_id` int(11) DEFAULT NULL COMMENT '当前登录用户关注的用户的id',
`is_valid` tinyint(1) DEFAULT 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=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?>
<project xmlns=&#34;http://maven.apache.org/POM/4.0.0&#34;
xmlns:xsi=&#34;http://www.w3.org/2001/XMLSchema-instance&#34;
xsi:schemaLocation=&#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&#34;>
<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: &#39;%d{HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n&#39;
复制代码
添加配置类
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配置类:
(, 下载次数: 1)
上传
点击文件名下载附件
关注/取关完成
业务逻辑
(, 下载次数: 1)
上传
点击文件名下载附件
Mapper完成
Mapper比较简单次要是查询关注信息、添加关注信息、取关或者再次关注。
(, 下载次数: 1)
上传
点击文件名下载附件
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(&#34;${service.name.ms-oauth-server}&#34;)
private String oauthServerName;
@Value(&#34;${service.name.ms-diners-server}&#34;)
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,
&#34;请选择要关注的人&#34;);
// 获取登录用户信息 (封装方法)
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,
&#34;关注成功&#34;, path, &#34;关注成功&#34;);
}
// 假如有关注信息,且目前处于关注形态,且要停止取关操作 -- 取关关注
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,
&#34;成功取关&#34;, path, &#34;成功取关&#34;);
}
// 假如有关注信息,且目前处于取关形态,且要停止关注操作 -- 重新关注
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,
&#34;关注成功&#34;, path, &#34;关注成功&#34;);
}
return ResultInfoUtil.buildSuccess(path, &#34;操作成功&#34;);
}
/**
* 添加关注列表到 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 + &#34;user/me?access_token={accessToken}&#34;;
ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
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(&#34;/{followUserId}&#34;)
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的用户。
(, 下载次数: 1)
上传
点击文件名下载附件
查看redis可以看到有两个集合,一个粉丝集合,一个关注集合。
(, 下载次数: 1)
上传
点击文件名下载附件
查看数据库,id为5的用户关注了id为1的用户
(, 下载次数: 1)
上传
点击文件名下载附件
让id等于7的用户关注id等于1的用户,redis和数据库存储信息如下:
(, 下载次数: 1)
上传
点击文件名下载附件
共同关注列表
从Redis中读取登录用户的关注列表与查看用户的关注列表,然后停止交集操作,获取共同关注的用户id
然后经过用户服务传入用户id数据获取用户基本信息
Controller添加方法
/**
* 共同关注列表
*
* @param userId
* @param access_token
* @return
*/
@GetMapping(&#34;commons/{userId}&#34;)
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 userId, String accessToken, String path) {
// 能否选择了查看对象
AssertUtil.isTrue(userId == null || userId < 1,
&#34;请选择要查看的人&#34;);
// 获取登录用户信息
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 + &#34;findByIds?access_token={accessToken}&ids={ids}&#34;,
ResultInfo.class, accessToken, StrUtil.join(&#34;,&#34;, 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(&#34;findByIds&#34;)
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(&#34;,&#34;);
List<ShortUserInfo> dinerInfos = usersMapper.findByIds(idArr);
return dinerInfos;
}
复制代码
Mapper:
/**
* 根据 ID 集合查询多个食客信息
* @param ids
* @return
*/
@Select(&#34;<script> &#34; +
&#34; select id, nickname, avatar_url from t_diners &#34; +
&#34; where is_valid = 1 and id in &#34; +
&#34; <foreach item=\&#34;id\&#34; collection=\&#34;ids\&#34; open=\&#34;(\&#34; separator=\&#34;,\&#34; close=\&#34;)\&#34;> &#34; +
&#34; #{id} &#34; +
&#34; </foreach> &#34; +
&#34; </script>&#34;)
List<ShortUserInfo> findByIds(@Param(&#34;ids&#34;) String[] ids);
复制代码
下面测试曾经让id5和7的用户关注了id为1的用户,我们继续让id5的用户关注id为3的用户,让id5、6、7的用户关注了id为2的用户:
redis和数据库信息如下:
(, 下载次数: 1)
上传
点击文件名下载附件
(, 下载次数: 1)
上传
点击文件名下载附件
(, 下载次数: 1)
上传
点击文件名下载附件
(, 下载次数: 1)
上传
点击文件名下载附件
测实验证
查询当前登录用户id为5和id为7的共同关注信息:
(, 下载次数: 1)
上传
点击文件名下载附件
查询当前登录用户id为6和id为7的共同关注信息:
(, 下载次数: 1)
上传
点击文件名下载附件
可以看出来5和7共同关注了1和2,6和7只共同关注了2,符合预期。
本文内容到此结束了
如有播种欢迎点赞
您的鼓励是我最大的动力
如有错误❌疑问 欢迎各位指出
欢迎光临 职贝云数AI新零售门户 (https://www.taojin168.com/cloud/)
Powered by Discuz! X3.5