- 目标:尽可能高效的将博主发布的文章推送给粉丝
功能点
- Feed流 (推送) 功能
- TimeLine 模式:不做内容筛选,按照内容发布时间排序,常用于好友或关注
- 智能排序:自动屏蔽违规、用户不感兴趣的内容。推送用户感兴趣的信息吸引用户
业务方案
-
方案一
- 推模式:也叫写扩散,即直接给其粉丝收件箱写入其发布内容
-
方案二(最优方案)
- 写扩散,推拉结合模式,也叫读写混合模式
- 业务逻辑
- 普通用户:推模式 (写扩散),直接给其粉丝收件箱写入其发布内容
- 大 V:推拉结合,活跃粉丝采用推模式,普通粉丝采用拉模式 (读扩散),等待用户自行读取
业务逻辑
- 请求方式:GET
- 请求路径:/blog/of/follow
- 请求参数
- lastId:上一次请求的最小时间戳
- offset:偏移量,即当前最小时间戳已经出现过的次数
- 返回值
- List<Blog>:小于指定时间戳的笔记集合
- minTime:此次查询结果的最小时间戳,供下次查询使用
- offset:偏移量,供下次查询使用
代码实现
BlogController
@RestController
@RequestMapping("/blog")
public class BlogController {
@Resource
private IBlogService blogService;
// 当前登录用户发布blog,同时推送给其粉丝
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
long blogId = blogService.saveBlog(blog);
return Result.ok(blogId);
}
// 当前登录用户查询目标用户的blog
@GetMapping("/of/follow")
public Result queryBlogOfFollow(@RequestParam("lastId") Long max, @RequestParam("offset", defaultValue = "0") Integer offset) {
ScrollResult blogPage = blogService.queryBlogOfFollow(max, offset);
return Result.ok(blogPage);
}
}
BlogServiceImpl
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IFollowService followService;
@Override
public long saveBlog(Blog blog) {
// 1. 获取登录用户id
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 2. 保存blog
boolean saveSuccess = save(blog);
if(!saveSuccess)
return Result.fail("保存笔记失败!");
// 3. 查询当前用户的所有粉丝
List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
// 4. 推送blog给所有粉丝
for (Follow follow : follows) {
// 4.1. 获取粉丝id
Long userId = follow.getUserId();
// 4.2. 推送blogId到粉丝的redis信箱中
String feedBoxKey = "feed:" + userId;
stringRedisTemplate.opsForZSet().add(feedBoxKey, blog.getId().toString(), System.currentTimeMillis());
}
// 5. 返回 id
return blog.getId();
}
@Override
public ScrollResult queryBlogOfFollow(Long max, Integer offset) {
// 1. 获取当前用户
Long userId = UserHolder.getUser().getId();
// 2. 查询收件箱 (ZREVRANGESCORE key max min LIMIT offset count)
String key = "feed:" + userId;
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 10);
// 3. 查询结果为空,直接返回
if(typedTuples == null || typedTuples.isEmpty())
return Result.ok();
// 4. 解析数据:blogId & minTime(时间戳) & offset
List<Long> ids = new ArrayList<>(typedTuples.size()); // 用于存储目标blogId集合
long minTime = 0; // 用于存储当前分页的最后一个时间戳
int os = 1; // 用于存储当前页面offset个数
for(ZSetOperations.TypeTuple<String> tuple : typedTyples) {
// 4.1. 获取id
ids.add( Long.valueOf(tuple.getValue()) );
// 4.2. 获取分数(时间戳)
long lastTime = minTime;
minTime = tuple.getScore().longValue();
os = lastTime == minTime ? os+1 : 1;
}
// 5. 查询blog
// 5.1. 根据id查询blog
String idsStr = StrUtil.join(",", ids);
List<Blog> blogs = query().in("id", ids).last( "ORDER BY FIELD(id, " + idsStr + ")").list();
// 5.2. 查询blog详情信息
for( Blog blog : blogs ) {
// 5.2.1. 查询给blog点赞的用户列表
queryBlogUser(blog);
// 5.2.2. 查询当前用户是否已经给blog点赞
isBlogLiked(blog);
}
// 6. 封装并返回
ScrollResult r = new ScrollResult();
r.setList(blogs);
r.setOffset(os);
r.setMinTime(minTime);
return r;
}
}
/dto/ScrollResult
@Data
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}