Redis入门到入土-章节三-实现点评系统

130 阅读5分钟

本章将基于点评系统扩展讲使用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);
}

以上就是两个基本功能实现的相关概念和代码片段了,下一张实现附近商户功能咯

image.png