本章将基于点评系统扩展讲使用Redis如何解决其中实际问题,主要会实现的功能有点赞,关注推送
点赞功能
需求:
- 一个用户只能点赞一次
实现:
- 点赞功能中,利用Redis的SortedSet集合判断是否点赞
- 用户点击某内容时,判断当前查看用户是否点过赞,有则展示在内容中
- 分页查询内容内容时,判断当前用户是否登录,如果有登录且对某些内容点赞,可展示在页面上
这里为什么不直接使用Set集合?
Set集合能实现基本功能,但是在存储数据上是无序的,使用SorteSet,可利用其中的socre排序,从而实现排序功能
public void bolgLikeAdd(Long bolgId) {
// 从本地线程中获取当前操作用户
Long userId = getUserId();
// redis中key
String key = "";
// 判断sortedset中是否存在当前用户信息
Double score = stringRedisTemplate.opsForZSet().score(key,userId);
// 不存在当前用户信息,此笔记的点赞数量+1,redis中添加当前用户信息
if(score == null) {
// 给数据库中数量+1
boolean updateState = update().setSql("liked = liked + 1").update();
// 成功后redis中添加数据
if(updateState){
stringRedisTemplate.opsForZSet()
.add(key,userId.toString,System.currentTimeMillis());
}
}
// 存在当前用户信息,数据库中笔记点赞数量-1,redis中移除当前用户信息
boolean updateState = update().setSql("liked = liked - 1").update();
// 成功后redis中添加数据
if(updateState){
stringRedisTemplate.opsForZSet()
.remove(key, userId.toString, System.currentTimeMillis());
}
}
排行
基于用户的点赞数据,当打开某个内容时,可查看到点赞的前几名头型或者其他想展示的
public Result queryBlogLikes(Long id) {
String key = BLOG_LIKED_KEY + id;
// 1.查询top5的点赞用户 zrange key 0 4
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if (top5 == null || top5.isEmpty()) {
return Result.ok(Collections.emptyList());
}
// 2.解析出其中的用户id
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
String idStr = StrUtil.join(",", ids);
// 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)
List<UserDTO> userDTOS = userService.query()
.in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list()
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
// 4.返回
return Result.ok(userDTOS);
}
这里在代码中对查询结果进行了流式处理,其实流式处理大家都知道,但是这里使用ORDER BY是干啥的?
使用Order by field解决数据库查询出数据顺序翻转,导致页面中排序时后点赞在前
关注推送
该方法主要是用来练习使用Redis实现简单消息队列功能
这里使用Redis实现消息队列是通过Feed流无限下拉获取最新消息方式实现
Feed流模式
- TimeLine:不做内容筛选,按照内容发布时间排序,用于还有或关注;
- 智能排序:推送用户感兴趣的内容
| TimeLine | 智能排序 |
|---|---|
| 信息全面,不会确实,实现简单 | 用户粘度高 |
| 信息过杂 | 算法不精准可能起反作用 |
实现方式
TimeLine拉模式:读扩散
用户发送消息时,发送的消息中会携带时间戳
当关注方收取消息时,将关注者发送的消息全部拉去,然后根据时间戳进行排序
收件箱收取完消息后,可将其中的消息清空,节省内存空间;但是每次读取消息时都要重新获取消息进行排序,延迟高
推模式:写扩散
用户发送的消息,将消息推送到粉丝收件箱中并用时间戳进行排序
如果粉丝很多,同时发送数据量将激增
推拉结合:读写混合
将接收用户分类,活跃用户采用推模式,普通用户接受消息采用拉模式;
活跃用户在接受消息时是已经拍好序的数据,普通用户在打开软件接受消息时进行排序再获取
节省内存的同时也解决了活跃用户体验
| 拉模式 | 推模式 | 推拉结合 | |
|---|---|---|---|
| 写比例 | 低 | 高 | 中 |
| 读比例 | 高 | 低 | 中 |
| 用户读取延迟 | 高 | 低 | 低 |
| 实现难度 | 复杂 | 简单 | 很复杂 |
| 使用场景 | 使用少 | 用户量少,没有大V | 千万用户,有大V |
基于推模式实现关注推送
1.发布笔记的同时,推送到粉丝收件箱
2.收件箱可根据时间戳排序--使用List或者sortedSet存储
3.查询收件箱数据时,实现分页
分页问题
feed流中的数据会不断更新,角标也在变化,使用传统分页方式,可能会出现数据重复;采用滚动分页代替
(再原本的分页中,新增了一条数据,此时查询第二页信息时,会从第一页最后一条数据开始计算下标)
滚动分页(每次记录查询的最后一条,下次从当前位置开始查询;第一次:0-5,此时插入一条数据,采用的头插法,所有数据角标向后移动,第二次:6-10)
list只能通过角标查询数据或者首尾查询,sortedSet支持score值范围查询,score可用时间戳代替,每次查询时记录最小的时间戳
滚动查询
分页参数:当前时间戳|上次查询最小时间戳;最小值;偏移量(上次查询与最小值一样的个数);数量
每次发布笔记时,存入sortedSet中,key为自定义字符串+粉丝id,value为笔记id,时间戳值,
当粉丝查询数据时,ZREVRANGEBYSCORE KEY MAX MIN WITHSCORE LIMIT OFFSET COUNT;反转根据score查询,范围为指定最大值到最小值,数量为从OFFSET-COUNT
public Result getUpperBlog(@RequestBody Map<String,String> requestMap){
...
}
public Result getUpperBlog(Map<String,String> requestMap){
// 请求数据
Long lastId = StringUtils.isEmpty(requestMap.get("lastId"))
?System.currentTimeMillis() :Long.valueOf(requestMap.get("lastId"));
Long offset = Long.valueOf(StringUtils.isEmpty(requestMap.get("offset"))
? "0" : requestMap.get("offset"));
// 从redis中查询数据
Long userId = UserHolder.getUser().getId();
String key = FEED_KEY + userId;
// 使用redis中sortedset保存的数据,根据以时间戳作为score的值进行排序然后查询
Set<ZSetOperations.TypedTuple<String>> dataFromRedis = stringRedisTemplate
.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, lastId, offset, 2);
if(dataFromRedis == null || dataFromRedis.isEmpty()){
return Result.ok();
}
// 指定List大小,防止空间浪费
List<Long> blogIds = new ArrayList<>(dataFromRedis.size());
int count = 1;
long minTime = 0;
for (ZSetOperations.TypedTuple<String> item : dataFromRedis) {
blogIds.add(Long.valueOf(item.getValue()));
// 最后的时间戳
long lastTime = item.getScore().longValue();
if(lastTime == 0){
count++;
}else {
minTime = lastTime;
count = 1;
}
}
// 查询笔记信息
String ids = StrUtil.join(",", blogIds);
List<Blog> blogs = query().in("id", blogIds).last("ORDER BY FIELD(id," + ids + ")").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(count);
r.setMinTime(minTime);
return Result.ok(r);
}
以上就是两个基本功能实现的相关概念和代码片段了,下一张实现附近商户功能咯