在探店图文的详情页中,可以关注发布笔记的作者:
需求:基于该表数据结构,实现两个接口:
① 关注和取关接口
② 判断是否关注的接口
关注是 User 之间的关系,是博主与粉丝的关系,数据库中有一张 tb_follow 表来标识:
注意:这里需要把主键修改为自增长
FollowController
@RestController
@RequestMapping("/follow")
public class FollowController {
@Autowired
private IFollowService followService;
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow){
return followService.follow(followUserId, isFollow);
}
@PutMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long followUserId){
return followService.isFollow(followUserId);
}
}
IFollowService
public interface IFollowService extends IService{ Result follow(Long followUserId, Boolean isFollow); Result isFollow(Long followUserId); }
FollowServiceImpl
@Service public class FollowServiceImpl extends ServiceImpl二、共同关注implements IFollowService { @Override public Result follow(Long followUserId, Boolean isFollow) { // 获取当前登录用户 Long userId = UserHolder.getUser().getId(); // 判断是否关注 if(isFollow){ // 关注,新增 Follow follow = new Follow(); follow.setFollowUserId(followUserId); follow.setUserId(userId); save(follow); } else { // 取消关注,删除 remove(new QueryWrapper ().eq("follow_user_id", followUserId).eq("user_id", userId)); } return Result.ok(); } @Override public Result isFollow(Long followUserId) { // 获取当前登录用户 Long userId = UserHolder.getUser().getId(); Integer count = query().eq("follow_user_id", followUserId).eq("user_id", userId).count(); return Result.ok(count > 0); } }
点击博主头像,可以进入博主首页:
博主个人首页依赖两个接口:
① UserController 根据 id 查询 User 信息
@GetMapping("/{id}")
public Result queryUserById(@PathVariable("id") Long userId){
// 查询详情
User user = userService.getById(userId);
if(user == null){
return Result.ok();
}
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
return Result.ok(userDTO);
}
② BlogController 根据 id 查询博主的探店笔记
@GetMapping("/of/user")
public Result queryBlogByUserId(
@RequestParam(value = "current", defaultValue = "1") Integer current,
@RequestParam("id") Long id) {
// 根据用户查询
Page page = blogService.query().
eq("user_id", id).
page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List records = page.getRecords();
return Result.ok(records);
}
需求:利用 Redis 中的 set 类型的数据结构,实现共同关注功能(set 数据结构可以用来计算指定 key 之间元素的交集)。在博主个人页面展示出当前用户与博主的共同好友。
FollowController
@RequestMapping("/follow")
public class FollowController {
@Autowired
private IFollowService followService;
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow){
return followService.follow(followUserId, isFollow);
}
@PutMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long followUserId){
return followService.isFollow(followUserId);
}
@GetMapping("/common/{id}")
public Result followCommons(@PathVariable("id") Long id){
return followService.followCommons(id);
}
}
IFollowService
public interface IFollowService extends IService{ Result follow(Long followUserId, Boolean isFollow); Result isFollow(Long followUserId); Result followCommons(Long id); }
FollowServiceImpl:对 follow 方法进行改进,在向数据库写入关注信息时,同时保存至 Redis 中,在取消关注时,除了删除数据库中的数据,同时移除 Redis 中的相关数据。followCommons 为查询共同关注的方法,使用 set 数据结构的特性进行查询。
@Service public class FollowServiceImpl extends ServiceImpl三、关注推送 3.1 方案分析implements IFollowService { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private IUserService userService; @Override public Result follow(Long followUserId, Boolean isFollow) { // 获取当前登录用户 Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; // 判断是否关注 if(isFollow){ // 关注,新增 Follow follow = new Follow(); follow.setFollowUserId(followUserId); follow.setUserId(userId); boolean isSuccess = save(follow); if (isSuccess) { stringRedisTemplate.opsForSet().add(key, followUserId.toString()); } } else { // 取消关注,删除 boolean isSuccess = remove(new QueryWrapper ().eq("follow_user_id", followUserId).eq("user_id", userId)); if (isSuccess) { stringRedisTemplate.opsForSet().remove(key, followUserId); } } return Result.ok(); } @Override public Result isFollow(Long followUserId) { // 获取当前登录用户 Long userId = UserHolder.getUser().getId(); Integer count = query().eq("follow_user_id", followUserId).eq("user_id", userId).count(); return Result.ok(count > 0); } @Override public Result followCommons(Long id) { // 获取当前登录用户 Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; String key2 = "follows:" + id; Set intersect = stringRedisTemplate.opsForSet().intersect(key, key2); if(intersect == null){ return Result.ok(Collections.emptyList()); } List ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList()); List collect = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList()); return Result.ok(collect); } }
关注推送也叫做 Feed 流,直译为投喂。为用户持续的提供”沉浸式“的体验,通过无限下拉刷新获取新的信息。
Feed 流产品有两种常见模式:
① Timeline:不做内容筛选,简单的按照内容发布时间排序,常用语好友或关注。例如朋友圈
优点:信息全面,不会有缺失,并且实现也相对简单
缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
② 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容,推送用户感兴趣的信息来吸引用户
优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
缺点:如果算法不精准,可能起到反作用
本例中的个人页面,是基于关注的好友来做 Feed 流,因此采用 Timeline 的模式。该模式的实现方案有三种:
① 拉模式
② 推模式
③ 推拉结合
**拉模式:**也叫做读扩散
假设有三个人,分别是张三、李四、王五,这三个人分别会在自己的账号发布自己的笔记或者视频,在这里我们统一称之为消息,这三个人发布的所有的消息都会被发送到发件箱中,发送到发件箱中的消息除了消息本身之外,还需要带有时间戳。粉丝赵六会有一个收件箱,平时收件箱是空的,只有他在读取消息时,才会把赵六关注的所有人的发件箱中的消息拉取到他的收件箱中,拉取到收件箱后,消息会按照携带的时间戳进行排序,然后赵六就可以读取消息了。
优点:节省内存空间。收件箱中的消息在读取完后就会被清空,下次需要读取的时候会重新从所有关注人的发件箱中拉取。消息只保存了一份。
缺点:每次读取消息都要重新拉取发件箱中的消息,然后再做排序,比较耗时。
**推模式:**也叫做写扩散
假设现在有两个 up 主:张三、李四,有三个粉丝:粉丝1、粉丝2、粉丝3,粉丝1关注了张三,粉丝2和3都关注了张三和李四,如果张三此时想要发送消息,那么这条消息会直接推送到张三的所有粉丝的收件箱中,而李四发送的消息也会被推送到粉丝2和3的收件箱中,收件箱收到消息后会对所有的消息进行排序,粉丝在读取消息时,就已经是排序好的消息了。这样的一个好处就是延时低,不必每次读取时都需要重新拉取消息。但这种方式内存占用会比较高,up 主每次发消息都要同步所有的粉丝,如果粉丝数过多,超过几百万,就需要复制几百万份。
推拉结合模式:也叫做读写混合,兼具推和拉两种模式的优点。
普通粉丝人数众多,但是活跃度较低,读取消息的频率也就低,可采用拉模式;
而活跃粉丝人数少,但是活跃度高,读取消息的频率高,可采用推模式。
大 V 发送消息时,会直接将消息推送到活跃粉丝的发件箱中,而针对于普通粉丝,消息会先发送到发件箱中,当普通粉丝读取消息时,会直接从发件箱中拉取。
三种模式对比:
需求:
① 修改新增探店笔记的业务,在保存 Blog 到数据库的同时,推送到粉丝的收件箱
② 收件箱满足可以根据时间戳排序,必须用 Redis 的数据结构实现
③ 查询收件箱数据时,可以实现分页查询



