黑马Redis实战项目——黑马点评笔记06 | 好友关注
- 1、关注和取关
- 2、共同关注
- 2.1 查看他人主页
- 2.2 查询共同关注
- A 改造关注和取关功能
- B 求交集
- 3、关注推送
- 3.1 Feed 流分析
- 3.1.1、拉模式(读扩散)
- 3.1.2、推模式(写扩散)
- 3.1.3、推拉结合模式(读写混合)
- 3.2 推送到粉丝收件箱
- 3.3 滚动分页
1、关注和取关
1、在FollowController中调用followService.follow和followService.isFollow方法
@RestController
@RequestMapping("/follow")
public class FollowController {
@Resource
private IFollowService followService;
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow){
return followService.follow(followUserId,isFollow);
}
@GetMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long followUserId){
return followService.isFollow(followUserId);
}
}
2、在IFollowService接口中声明 follow 和 isFollow 方法
public interface IFollowService extends IService<Follow> {
Result follow(Long followUserId, Boolean isFollow);
Result isFollow(Long followUserId);
}
3、在FollowServiceImpl类中实现方法
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
/**
* 关注和取关
* @param followUserId
* @param isFollow
* @return
*/
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//获取当前用户Id
Long userId = UserHolder.getUser().getId();
//1、判断是否已经关注
if(isFollow) {
//2 未关注,新增数据
Follow follow = new Follow();
follow.setFollowUserId(followUserId);
follow.setUserId(userId);
save(follow);
}else{
//3 已关注,删除delete from tb_follow where userId = ? and follow_user_id = ?
QueryWrapper<Follow> followQueryWrapper = new QueryWrapper<>();
followQueryWrapper.eq("user_id",userId).eq("follow_user_id",followUserId);
remove(followQueryWrapper);
}
return Result.ok();
}
/**
* 查询是否已经关注
* @param followUserId
* @return
*/
@Override
public Result isFollow(Long followUserId) {
//获取当前用户Id
Long userId = UserHolder.getUser().getId();
//1 查询是否关注
Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
return Result.ok(count>0);
}
}
2、共同关注
2.1 查看他人主页
1、在UserController新增根据id查询用户功能
@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);
}
2、在BlogController新增根据用户id查询blog功能
@GetMapping("/of/user")
public Result queryBlogByUserId(
@RequestParam(value = "current", defaultValue = "1") Integer current,
@RequestParam("id") Long id) {
// 根据用户查询
Page<Blog> page = blogService.query()
.eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
return Result.ok(records);
}
2.2 查询共同关注
利用redis中set数据操作的求交集功能实现共同关注。
A 改造关注和取关功能
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 关注和取关
* @param followUserId
* @param isFollow
* @return
*/
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//获取当前用户Id
Long userId = UserHolder.getUser().getId();
String key = "follows:"+userId;
//1、判断是否已经关注
if(isFollow) {
//2 未关注,新增数据
Follow follow = new Follow();
follow.setFollowUserId(followUserId);
follow.setUserId(userId);
boolean isSuccess = save(follow);
if(isSuccess){
//把关注用户的id放入Redis的set集合
stringRedisTemplate.opsForSet().add(key,followUserId.toString());
}
}else{
//3 已关注,删除delete from tb_follow where userId = ? and follow_user_id = ?
QueryWrapper<Follow> followQueryWrapper = new QueryWrapper<>();
followQueryWrapper.eq("user_id",userId).eq("follow_user_id",followUserId);
boolean isSuccess = remove(followQueryWrapper);
if(isSuccess){
//把关注用户的id从Redis的set集合中移除
stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
}
}
return Result.ok();
}
}
B 求交集
1、FollowController类中调用followService.followCommons
@GetMapping("/common/{id}")
public Result followCommons(@PathVariable("id") Long id){
return followService.followCommons(id);
}
2、IFollowService接口中声明followCommons方法
Result followCommons(Long id);
3、FollowServiceImpl中实现followCommons方法
@Resource
private IUserService userService;
@Override
public Result followCommons(Long id) {
//1 获取当前用户Id
Long userId = UserHolder.getUser().getId();
String key = "follows:"+userId;
//2 求交集
String key2 = "follows:"+id;
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
if(intersect==null || intersect.isEmpty()){
//无交集
return Result.ok(Collections.emptyList());
}
//3 解析id集合
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
//4 查询用户
List<UserDTO> users = userService.listByIds(ids)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(users);
}
3、关注推送
3.1 Feed 流分析
3.1.1、拉模式(读扩散)
3.1.2、推模式(写扩散)
3.1.3、推拉结合模式(读写混合)
本案例用户不多且无v,使用推模式.
3.2 推送到粉丝收件箱
传统角标分页:
滚动分页:如果是数据有变化的情况,尽量不要使用List。因为List不支持滚动分页,只能按照角标查询,而sortedSet虽然根据socre排名查询和按角标查是一样的,但它可以按照score值的范围查询。每次查询记录最小的score(时间戳),下一次查询时查更小的时间戳则实现了滚动分页,不会重复查询。
1、修改新增博客的功能接口,使博客发布就能推送到粉丝
//修改BlogController中saveBlog
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
return blogService.saveBlog(blog);
}
//IBlogService接口中声明saveBlog
Result saveBlog(Blog blog);
//类中实现saveBlog
@Resource
private IFollowService followService;
@Override
public Result saveBlog(Blog blog) {
//1 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
//2 保存探店笔记
boolean isSuccess = save(blog);
if (!isSuccess){
return Result.fail("发布失败,请检查重试");
}
//3 查询博文作者的所有粉丝select * from tb_follow where follow_user_id = ?
List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
//4 推送笔记id给所有粉丝
for (Follow follow : follows) {
//4.1 获取粉丝id
Long userId = follow.getUserId();
//4.2 推送笔记到每个粉丝的收件箱(sortedSet)
String key = "feed:" + userId;
stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
}
//5 返回id
return Result.ok(blog.getId());
}
3.3 滚动分页
展示推送的blog消息,使用的滚动分页,稍许复杂
基于Redis的sortedSet实现滚动分页所需参数:
基于Redis的sortedSet实现滚动分页代码实现:
1、一个dto类封装滚动分页返回值
@Data
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}
2、滚动查询,展示博主推送的笔记
//BlogController中调用queryBlogOfFollow方法
@GetMapping("/of/follow")
public Result queryBlogOfFollow(//如果offset为空说明是第一次传入,设置默认值0
@RequestParam("lastId") Long max, @RequestParam(value = "offset",defaultValue = 0) Integer offset){
return blogService.queryBlogOfFollow(max,offset);
}
//IBlogService接口声明queryBlogOfFollow方法
Result queryBlogOfFollow(Long max, Integer offset);
//BlogServiceImpl类实现queryBlogOfFollow方法
/**
* 滚动查询,展示博主推送的笔记,
* 新发布的滚动查询查不到,但是往上滚,前端做了处理,就是刷新重新查询,开始位置在当前最新位置
* @param max
* @param offset
* @return
*/
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
//1 获取当前用户
Long userId = UserHolder.getUser().getId();
//2 查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count
// limit是小于等于的意思,小于等于查询的最后时间戳
String key = FEED_KEY + userId;
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 5);
//3 非空判断
if (typedTuples == null || typedTuples.isEmpty()){
return Result.ok();
}
//4 解析数据: blogId,minTime(时间戳),offset
ArrayList<Long> ids = new ArrayList<>(typedTuples.size());
long minTime = 0; //这个minTime是上次查询的最小时间戳,作为当次查询的最大时间戳来开始查
int os = 1;//os->offset
for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
//4.1 获取博客id转换为Long型并存入ids数组中
ids.add(Long.valueOf(typedTuple.getValue()));
//4.2 获取分数(时间戳) 判读得到最后一次的时间戳,以及偏移量
long time = typedTuple.getScore().longValue();
if (time == minTime){
os++;
}else {
minTime = time;
os = 1;
}
}
//5 根据id查询blog,先把前面保存id的ids数组转为字符串
String idStr = StrUtil.join(",", ids);
//由于用mp提供的listByIds是用in方法查,不能保证顺序,需要orderby手动排序
List<Blog> blogs = query().in("id", ids).last("order by field(id," + idStr + ")").list();
for (Blog blog : blogs) {
//5.1 查询blog有关用户信息
queryBlogUser(blog);
//5.2 查询blog是否已被点赞
isBlogLiked(blog);
}
//6 封装并返回
ScrollResult r = new ScrollResult();
r.setList(blogs);
r.setOffset(os);
r.setMinTime(minTime);
return Result.ok(r);
}