redis-探店&关注

1 达人探店

1.1 分享探店图文

1.1.1 发布探店笔记

涉及两个数据库表:

  • tb_blog:探店笔记表,包含笔记中的标题、文字、图片等
  • tb_blog_comments:其他用户对探店笔记的评价

发布流程:

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j
@RestController
@RequestMapping("upload")
public class UploadController {

@PostMapping("blog")
public Result uploadImage(@RequestParam("file") MultipartFile image) {
try {
// 获取原始文件名称
String originalFilename = image.getOriginalFilename();
// 生成新文件名
String fileName = createNewFileName(originalFilename);
// 保存文件
image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));
// 返回结果
log.debug("文件上传成功,{}", fileName);
return Result.ok(fileName);
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static final String IMAGE_UPLOAD_DIR = "xxxx";

@RestController
@RequestMapping("/blog")
public class BlogController {

@Resource
private IBlogService blogService;

@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
//获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUpdateTime(user.getId());
//保存探店博文
blogService.saveBlog(blog);
//返回id
return Result.ok(blog.getId());
}
}

1.1.2 查看探店笔记

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/hot")
public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
return blogService.queryHotBlog(current);
}

/**
* @param
* @return com.hmdp.dto.Result
* @description //查看笔记详情页面
**/
@GetMapping("/{id}")
public Result queryBlogById(@PathVariable("id") Long id) {
return blogService.queryBlogById(id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;

@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(this::queryBlogUser);
return Result.ok(records);
}

/**
* @param
* @return com.hmdp.dto.Result
* @description //查看笔记详情页面
**/
@Override
public Result queryBlogById(Long id) {
// 1.查询blog
Blog blog = getById(id);
if (null == blog) {
return Result.fail("笔记不存在!");
}
// 2.查询blog相关的用户
queryBlogUser(blog);
return Result.ok(blog);
}

private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}

1.2 点赞功能

1
2
3
4
5
6
@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable("id") Long id) {
//修改点赞数量
blogService.update().setSql("liked = liked +1 ").eq("id",id).update();
return Result.ok();
}

这样会有一个问题,一个用户可以多次的点赞

1.2.1 修改

  1. 给Blog类中添加一个isLike字段,标示是否被当前用户点赞

    1
    2
    @TableField(exist = false)
    private Boolean isLike;
  2. 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1

  3. 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段

  4. 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段

1
2
3
4
5
6
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
// 修改点赞数量 update tb_blog set liked = liked where id = ?
//blogService.update().setSql("liked = liked + 1").eq("id", id).update();
return blogService.likeBlog(id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;

@Resource
StringRedisTemplate stringRedisTemplate;

@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(new Consumer<Blog>() {
@Override
public void accept(Blog blog) {
queryBlogUser(blog);
isBlogLiked(blog);
}
});
//records.forEach(this::queryBlogUser);
return Result.ok(records);
}

/**
* @param
* @return com.hmdp.dto.Result
* @description //查看笔记详情页面
**/
@Override
public Result queryBlogById(Long id) {
// 1.查询blog
Blog blog = getById(id);
if (null == blog) {
return Result.fail("笔记不存在!");
}
// 2.查询blog相关的用户
queryBlogUser(blog);
// 3.查询blog是否被点赞
isBlogLiked(blog);
return Result.ok(blog);
}

/**
* @param
* @return void
* @description //当前笔记是否被当前用户点赞
**/
private void isBlogLiked(Blog blog) {
Long id = blog.getId();
// 1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 2.判断当前登录用户是否点赞 key "blog:liked:" + id
String key = RedisConstants.BLOG_LIKED_KEY + id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
boolean flag = BooleanUtil.isTrue(isMember);
blog.setIsLike(flag);
}

/**
* @param
* @return com.hmdp.dto.Result
* @description // 点赞
**/
@Override
public Result likeBlog(Long id) {
// 1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 2.判断当前登录用户是否点赞 key "blog:liked:" + id
String key = RedisConstants.BLOG_LIKED_KEY + id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
boolean flag = BooleanUtil.isTrue(isMember);

if (!flag) {
// 3.如果未点赞,可以点赞
// 3.1数据库点赞数+1
// 修改点赞数量 update tb_blog set liked = liked where id = ?
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
if (isSuccess) {
// 3.2保存用户到Redis的set集合
stringRedisTemplate.opsForSet().add(key, userId.toString());
}

} else {
// 4.如果已经点赞,取消点赞
// 4.1数据库点赞数 -1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();

if (isSuccess) {
// 4.2把用户从Redis的set集合移除
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
}

return null;
}

private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}

1.3 基于List实现点赞用户列表

在探店笔记的详情页面,应该把给该笔记点赞的人显示出来,比如最早点赞的TOP5,形成点赞排行榜:

之前的点赞是放到set集合,但是set集合是不能排序的,所以这个时候,咱们可以采用一个可以排序的set集合,就是咱们的sortedSet

在这里插入图片描述

1.3.1 List/Set/SortedSet对比

在这里插入图片描述

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;

@Resource
StringRedisTemplate stringRedisTemplate;

@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(new Consumer<Blog>() {
@Override
public void accept(Blog blog) {
queryBlogUser(blog);
isBlogLiked(blog);
}
});
//records.forEach(this::queryBlogUser);
return Result.ok(records);
}

/**
* @param
* @return com.hmdp.dto.Result
* @description //查看笔记详情页面
**/
@Override
public Result queryBlogById(Long id) {
// 1.查询blog
Blog blog = getById(id);
if (null == blog) {
return Result.fail("笔记不存在!");
}
// 2.查询blog相关的用户
queryBlogUser(blog);
// 3.查询blog是否被点赞
isBlogLiked(blog);
return Result.ok(blog);
}

/**
* @param
* @return void
* @description //当前笔记是否被当前用户点赞(Set)
**/
private void isBlogLikedSet(Blog blog) {
Long id = blog.getId();
// 1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
if (null == userId) {
return;
}
// 2.判断当前登录用户是否点赞 key "blog:liked:" + id
String key = RedisConstants.BLOG_LIKED_KEY + id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
boolean flag = BooleanUtil.isTrue(isMember);
blog.setIsLike(flag);
}

/**
* @param
* @return void
* @description //当前笔记是否被当前用户点赞(SortedSet)
**/
private void isBlogLiked(Blog blog) {
Long id = blog.getId();
// 1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
if (null == userId) {
return;
}
// 2.判断当前登录用户是否点赞 key "blog:liked:" + id
String key = RedisConstants.BLOG_LIKED_KEY + id;
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
blog.setIsLike(null != score);
}

/**
* @param
* @return com.hmdp.dto.Result
* @description // 点赞(Set集合)
**/
public Result likeBlogSet(Long id) {
// 1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 2.判断当前登录用户是否点赞 key "blog:liked:" + id
String key = RedisConstants.BLOG_LIKED_KEY + id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
boolean flag = BooleanUtil.isTrue(isMember);

if (!flag) {
// 3.如果未点赞,可以点赞
// 3.1数据库点赞数+1
// 修改点赞数量 update tb_blog set liked = liked where id = ?
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
if (isSuccess) {
// 3.2保存用户到Redis的set集合
stringRedisTemplate.opsForSet().add(key, userId.toString());
}

} else {
// 4.如果已经点赞,取消点赞
// 4.1数据库点赞数 -1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();

if (isSuccess) {
// 4.2把用户从Redis的set集合移除
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
}

return null;
}

/**
* @param
* @return com.hmdp.dto.Result
* @description // 点赞(SortedSet集合)
**/
@Override
public Result likeBlog(Long id) {
// 1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 2.判断当前登录用户是否点赞 key "blog:liked:" + id
String key = RedisConstants.BLOG_LIKED_KEY + id;
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());

if (null == score) {
// 3.如果未点赞,可以点赞
// 3.1数据库点赞数+1
// 修改点赞数量 update tb_blog set liked = liked where id = ?
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
if (isSuccess) {
// 3.2保存用户到Redis的sortedset集合 zadd key score member
stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
}

} else {
// 4.如果已经点赞,取消点赞
// 4.1数据库点赞数 -1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();

if (isSuccess) {
// 4.2把用户从Redis的set集合移除
stringRedisTemplate.opsForZSet().remove(key, userId.toString());
}
}

return null;
}

private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}

BlogController.java

1
2
3
4
5
@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable("id") Long id) {
return blogService.queryBlogLikes(id);
}

BlogServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @param
* @return com.hmdp.dto.Result
* @description //查询点赞列表
**/
@Override
public Result queryBlogLikes(Long id) {
String key = RedisConstants.BLOG_LIKED_KEY + id;
// 1.查询top5的点赞用户 zrange key 0 4
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if (null == top5 || top5.isEmpty()) {
return Result.ok(Collections.emptyList());
}

// 2.解析出其中的用户id
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());

// 3.根据用户id查询用户
List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());

// 4.返回
return Result.ok(userDTOS);
}

此时会发现点赞的顺序是反的,问题就出现在了SQL上
在这里插入图片描述

那么此时修改SQL即可解决

1
SELECT * FROM tb_user WHERE id IN ('5','1') ORDER BY FIELD(id,5,1);
1
2
3
4
5
6
7
8
9
// 3.根据用户id查询用户
//List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
List<UserDTO> userDTOS = userService.query()
// SELECT * FROM tb_user WHERE id IN ('5','1') ORDER BY FIELD(id,5,1);
.in("id", ids)
.last("order by field (id," + idsStr + ") ").list()
.stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());

2 关注列表

2.1 添加关注

针对用户的操作:可以对用户进行关注和取消关注功能。

在这里插入图片描述

涉及到数据库表tb_follow:
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController
@RequestMapping("/follow")
public class FollowController {
@Autowired
private IFollowService iFollowService;

/**
* @param
* @return com.hmdp.dto.Result
* @description //关注博主
**/
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") boolean isFollow) {
return iFollowService.follow(followUserId, isFollow);
}

/**
* @param
* @return com.hmdp.dto.Result
* @description //判断是否关注
**/
@GetMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long followUserId) {
return iFollowService.isFollow(followUserId);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {

/**
* @param
* @return com.hmdp.dto.Result
* @description //关注
**/
@Override
public Result follow(Long followUserId, boolean isFollow) {
Long userId = UserHolder.getUser().getId();
// 1.判断是关注还是取关
if (isFollow) {
// 2.关注,新增数据
Follow follow = new Follow();
follow.setFollowUserId(followUserId);
follow.setUserId(userId);
// insert into tb_follow values()
save(follow);
} else {
// 3.取关,删除
// delete from follow where user_id = ? and follow_user_id = ?
LambdaQueryWrapper<Follow> wrapper = new LambdaQueryWrapper<>();
remove(wrapper.eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId));
}

return Result.ok();
}

/**
* @param
* @return com.hmdp.dto.Result
* @description //判断是否关注
**/
@Override
public Result isFollow(Long followUserId) {
// 1.获取用户id
Long userId = UserHolder.getUser().getId();
// 2.查询是否关注 select * from tb_follow where user_id = ? and follow_user_id = ?
Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
return Result.ok(count > 0);
}
}

2.2 共同关注列表

想要去看共同关注的好友,需要首先进入到这个页面,这个页面会发起两个请求

1、去查询用户的详情

2、去查询用户的笔记

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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);
}

// BlogController 根据id查询博主的探店笔记
@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);
}

在这里插入图片描述

但是此时点击了之后发现共同关注报错,如何去解决共同关注的问题。模拟场景:A关注了B、C,而D关注了C、E,那么对于A和D来说他们的共同关注就是C,在Redis中可以使用set求交集

2.2.1 修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @param
* @return com.hmdp.dto.Result
* @description //关注
**/
@Override
public Result follow(Long followUserId, boolean isFollow) {
Long userId = UserHolder.getUser().getId();
String key = RedisConstants.FOLLOW_USER_LIST + userId;
// 1.判断是关注还是取关
if (isFollow) {
// 2.关注,新增数据
Follow follow = new Follow();
follow.setFollowUserId(followUserId);
follow.setUserId(userId);
// insert into tb_follow values()
boolean isSuccess = save(follow);

// 加入Redis,实现共同关注
if (isSuccess) {
// 把关注用户的id,加入redis的set集合 sadd userId followUserId
stringRedisTemplate.opsForSet().add(key, followUserId.toString());
}


} else {
// 3.取关,删除
// delete from follow where user_id = ? and follow_user_id = ?
LambdaQueryWrapper<Follow> wrapper = new LambdaQueryWrapper<>();
boolean isSuccess = remove(wrapper.eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId));
if (isSuccess) {
// 从Redis中移除,实现取关
// 把关注用户的id,从redis的set集合中移除 srem userId followUserId
stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
}

}

return Result.ok();
}

2.2.2 共同关注

1
2
3
4
5
6
7
8
9
/**
* @param
* @return com.hmdp.dto.Result
* @description //查询共同关注
**/
@GetMapping("/common/{id}")
public Result followCommons(@PathVariable("id") Long id) {
return iFollowService.followCommons(id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @param
* @return com.hmdp.dto.Result
* @description //共同关注
**/
@Override
public Result followCommons(Long id) {
// 1.获取当前用户
Long userId = UserHolder.getUser().getId();
String key = RedisConstants.FOLLOW_USER_LIST + userId;

// 目标用户
String keyFollow = RedisConstants.FOLLOW_USER_LIST + id;

// 2.求交集
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, keyFollow);
if (null == intersect || intersect.isEmpty()) {
return Result.ok(Collections.emptyList());
}

// 3.解析id集合
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
List<UserDTO> userDTOS = userService.listByIds(ids)
.stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());

// 查询用户
return Result.ok(userDTOS);
}

2.3 关注用户发布消息-Feed流

当我们关注了用户后,这个用户发了动态,那么我们应该把这些数据推送给用户,这个需求,其实我们又把他叫做Feed流,关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。

对于传统的模式的内容解锁:我们是需要用户去通过搜索引擎或者是其他的方式去解锁想要看的内容

在这里插入图片描述

对于新型的Feed流的的效果:不需要我们用户再去推送信息,而是系统分析用户到底想要什么,然后直接把内容推送给用户,从而使用户能够更加的节约时间,不用主动去寻找。

在这里插入图片描述

Feed流产品有两种常见模式:
Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈

优点:信息全面,不会有缺失。并且实现也相对简单
缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户

优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
缺点:如果算法不精准,可能起到反作用
本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:

  • 拉模式
  • 推模式
  • 推拉结合

2.3.1 拉模式-读扩散

该模式的核心含义就是:当张三和李四和王五发了消息后,都会保存在自己的邮箱中,假设赵六要读取信息,那么他会从读取他自己的收件箱,此时系统会从他关注的人群中,把他关注人的信息全部都进行拉取,然后在进行排序

优点:比较节约空间,因为赵六在读信息时,并没有重复读取,而且读取完之后可以把他的收件箱进行清楚。

缺点:比较延迟,当用户读取数据时才去关注的人里边去读取数据,假设用户关注了大量的用户,那么此时就会拉取海量的内容,对服务器压力巨大。

2.3.2 推模式-写扩散

推模式是没有写邮箱的,当张三写了一个内容,此时会主动的把张三写的内容发送到他的粉丝收件箱中去,假设此时李四再来读取,就不用再去临时拉取了

优点:时效快,不用临时拉取

缺点:内存压力大,假设一个大V写信息,很多人关注他, 就会写很多分数据到粉丝那边去

2.3.3 推拉结合-读写混合

推拉模式是一个折中的方案,站在发件人这一段,如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力,如果是大V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去,现在站在收件人这端来看,如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来,而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息。

2.4 探店推送

需要实现当关注用户增加一个新的笔记后,能推送到分析收件箱

2.4.1 分析

Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。

传统的分页在feed流是不适用的,因为我们的数据会随时发生变化。

假设在t1 时刻,我们去读取第一页,此时page = 1 ,size = 5 ,那么我们拿到的就是10 ~ 6 这几条记录,假设现在t2时候又发布了一条记录,此时t3 时刻,我们来读取第二页,读取第二页传入的参数是page=2 ,size=5 ,那么此时读取到的第二页实际上是从6 开始,然后是6~2 ,那么我们就读取到了重复的数据,所以feed流的分页,不能采用原始方案来做。

在这里插入图片描述

2.4.2 Feed流滚动分页

我们需要记录每次操作的最后一条,然后从这个位置开始去读取数据

举个例子:我们从t1时刻开始,拿第一页数据,拿到了10~6,然后记录下当前最后一次拿取的记录,就是6,t2时刻发布了新的记录,此时这个11放到最顶上,但是不会影响我们之前记录的6,此时t3时刻来拿第二页,第二页这个时候拿数据,还是从6后一点的5去拿,就拿到了5-1的记录。我们这个地方可以采用sortedSet来做,可以进行范围查询,并且还可以记录当前获取数据时间戳最小值,就可以实现滚动分页了

在这里插入图片描述

即:在保存完探店笔记后,获得到当前笔记的粉丝,然后把数据推送到粉丝的redis中去。

1
2
3
4
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
return blogService.saveBlog(blog);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @param
* @return com.hmdp.dto.Result
* @description //使用push模式
**/
@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 user_id from tb_follow where follow_user_id = ?
LambdaQueryWrapper<Follow> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Follow::getFollowUserId, user.getId());
List<Follow> follows = followService.list(wrapper);
// 4.推送笔记id给所有粉丝
for (Follow follow : follows) {
// 4.1获取粉丝id
Long fansId = follow.getUserId();
// 4.2推送给粉丝
String key = RedisConstants.FEED_KEY + fansId;
stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
}


// 5.返回id
return Result.ok(blog.getId());
}

2.5 好有关注页分页查询

在个人主页的“关注”卡片中,查询并展示推送的Blog信息:

在这里插入图片描述

正序:

在这里插入图片描述

倒序:

在这里插入图片描述

带分数:

在这里插入图片描述

滚动分页规律:

1
2
3
4
-- 第一次
ZREVRANGEBYSCORE key 设定一个最大值 0 WITHSCORES LIMIT 0 每页展示几条
-- 之后
ZREVRANGEBYSCORE key 第一条最小的角标 0 WITHSCORES LIMIT 第一页中与最小值相等的元素的个数 每页展示几条

2.5.1 新增实体类

1
2
3
4
5
6
@Data
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}
1
2
3
4
5
@GetMapping("/of/follow")
public Result queryBlogOfFollow(
@RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){
return blogService.queryBlogOfFollow(max, offset);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* @param
* @return com.hmdp.dto.Result
* @description //滚动分页查询关注列表
**/
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
// 1.查询当前用户
Long userId = UserHolder.getUser().getId();
// 2.找到收件箱
String key = RedisConstants.FEED_KEY + userId;

// 滚动分页查询(第一次查询)
//ZREVRANGEBYSCORE z1 1000 0 WITHSCORES LIMIT 0 5
Set<ZSetOperations.TypedTuple<String>> set = stringRedisTemplate
.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 2L);

// 3.非空判断
if (null == set || set.isEmpty()) {
return Result.ok();
}

// 4.解析收件箱的数据:blogId,minTime(时间戳),offset
ArrayList<Long> ids = new ArrayList<>(set.size());
long minTime = Long.MAX_VALUE;
int off = 1;
for (ZSetOperations.TypedTuple<String> tuple : set) {
// 4.1获取id
ids.add(Long.valueOf(tuple.getValue()));
// 4.2 获取分数(时间戳)
long time = tuple.getScore().longValue();
if (time == minTime) {
off++;
} else {
minTime = minTime < tuple.getScore().longValue() ? minTime : time;
off = 1;
}
}
//List<Blog> blogs = listByIds(ids);
// select * from tb_blog where id in () order by field(id,max,min)
String joinStr = StrUtil.join(",", ids);
// 5.根据blogId查询blog
List<Blog> blogs = query().in("id", ids).last("order by field(id," + joinStr + ")").list();
for (Blog blog : blogs) {
// 2.查询blog相关的用户
queryBlogUser(blog);
// 3.查询blog是否被点赞
isBlogLiked(blog);
}

// 6.封装并返回
ScrollResult result = new ScrollResult();
result.setList(blogs);
result.setOffset(off);
result.setMinTime(minTime);

return Result.ok(result);
}

redis-探店&关注
https://baijianglai.cn/redis-探店-关注/47cab89a5a24/
作者
Lai Baijiang
发布于
2024年3月10日
许可协议