基于Redis的一些业务实现

1. Redis实现点赞

点赞业务可以有多种实现,常见的有两种:

  • 在数据库表中增加字段,已经点赞了就设为1,没有点赞就设为0,但是与数据库的频繁交互使得效率不高
  • 基于Redis的实现,用户点赞后,将用户存入redis,在用户操作前,先判断redis中是否有该用户,如果没有,可以点赞,并且数据库点赞数+1,如果有,则可以取消点赞,数据库点赞数-1。并且为了方便后续业务的拓展,我们这里采用sorted_set这种数据结构,既不能存重复值,又能够实现排序。
@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public Result isLike(Long id) {
	// 1.获取登录用户 (我这里在用户登录之后,将用户存在了ThreadLocal里面,方便后续取出)
	Long userId = UserHolder.getUser.getId();  // UserHolder是我编写的一个操作ThreadLocal的工具类,也可以直接使用ThreadLocal操作。
	// 2.判断当前用户是否已经点赞
	String key = "blog:liked:" + id;   // 首先拼接key,在id前面加上业务名称,更有结构性
	// 3.由于我们使用的sorted_set,不能直接判断是否存在该元素,只能通过查询它所对应的分数,分数为null,则为不存在
	Double score = stringRedsiTemplate.opsForZSet().score(key,userId.toString());

	if(score == null) {
		// 3.未点赞,可以点赞
		// 3.1 数据库点赞数+1
		boolean isSuccess = update().setSql("liked = liked + 1").eq("id",id).update();
		// 3.2 保存用户id到redis中
		if(isSuccess) {
			stringRedisTemplate.opsForZSet.add(key,userId.toString(),System.currentTimeMillis());// 这里用时间戳来表示分数,就可以实现排序了。
		}
	}else {
		// 4.已经点赞了,可以取消点赞
		// 4.1 数据库点赞数-1
		boolean isSuccess = update().setSql("liked = liked - 1").eq("id",id).update();
		// 4.2 把用户从redis中删除
		if(isSuccess) {
			stringRedisTemplate.opsForZSet().remove(key,userId.tostring());
		}
	}
	return Result.ok();
}

2. 点赞列表查询

基于上面的点赞业务,我们可以实现点赞列表的查询业务

@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public Result queryBlogLikes(Long id) {
	// 1.查询前五名的点赞用户 zrange key 0 4
	String key = "blog:liked:" + id;   // 首先还是拼接key
	Set<String> top5 = stringReidsTemplate.opsForZSet().range(key,0,4);
	if(top5 == null || top5.isEmplty()){
		return Result.ok();
	}
	// 2.从set集合中解析出用户id
	List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
	// 3.根据id查询用户,但是为了保护用户隐私,我们只展示用户一些不重要的信息,比如昵称头像,因此我编写了一个UserDTO,将查询出来的User -> UserDTO
	List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(UserDTO.class)).collect(Collectors.toList());
	// 4.返回
	return Result.ok(userDTOS);
}

3. 关注和取关

@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public Result follow(Long followUserId,Boolean isFollow) {
	// 1.获取登录用户
	UserDTO = UserHolder.getUser();
	if(user == null) {
		return Result.ok();
	}
	Long userId = user.getId();
	// 2.判断是关注还是取关
	if(isFollow) {
		// 3.关注,新增数据
		Follow follow = new Follow();
		follow.setUserId(userId);
		follow.setFollowUserId(followUserId);
		boolean isSuccess = save(follow);
		if(isSuccess) {
             // 把关注用户的id,放入redis的set集合,便于后续查询共同关注 sadd userId followUserId
             String key = "follows:" + userId;
             stringRedisTemplate.opsForSet().add(key,followUserId.toString());
		}else {
            // 4.取关,删除数据 delete from tb_follow where userId = ? and follow_user_id = ?
            remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",followUserId));
            // 把关注的用户id移除redis
            if (isSuccess) {
                String key = "follows:" + userId;
                stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
            }
        }
        return Result.ok();
	}
 // 查询是否关注
 @Override
 public Result isFollow(Long followUserId) {
     // 获取用户id
     Long userId = UserHolder.getUser().getId();
     // 查询是否存在 select count from tb_follow where user_id = ? and follow_user_id = ?
     Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
     // 判断count是否 > 0
     return Result.ok(count > 0);
}

4. 共同关注

利用redis中的set数据结构,实现共同关注功能,在博主的个人主页展示出当前用户和博主的共同关注

    // 共同关注
    @Override
    public Result followCommons(Long id) {
        // 获取当前用户
        Long userId = UserHolder.getUser().getId();
        String key = "follows:" + userId;
        String key2 = "follows:" + id;
        // 求交集
        Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
        if (intersect == null || intersect.isEmpty()) {
            return Result.ok();
        }
        // 解析id集合
        List<Long> list = intersect.stream().map(Long::valueOf).collect(Collectors.toList());

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

        return Result.ok(userDTOS);
    }

5. 关注推送

关注推送也叫feed流,直译为投喂。为用户持续提供沉浸式体验,通过无限下拉刷新获取新的信息
在这里插入图片描述
Feed流的模式
Feed流产品有两种常见的模式:

  • TimeLine流:不做内筒筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈

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

    • 优点:投喂用户感兴趣信息,用户粘度高,容易沉迷
    • 缺点:如果算法不精确,可能起到反作用

我们这里实现基于推模式的关注推送功能

Feed流的滚动分页:由于Feed流中的数据会不断更新,因此数据的角标也在不断变化,因此不能采用传统的分页模式 (list和sorted_list都能实现排序,但list不能实现滚动分页,因此我们使用sorted_set)

    @Override
    public Result queryBlogOfFollow(Long max, Integer offset) {
        // 1.获取当前用户
        Long userId = UserHolder.getUser().getId();
        // 2.查询收件箱 zrevrangebyscore key Max Min Limit offset count
        String key = "feed:" + userId;
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
                .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
        // 3.非空判断
        if (typedTuples == null || typedTuples.isEmpty()) {
            return Result.ok();
        }
        // 4.解析数据:blogId,minTime(时间戳),offset
        List<Long> ids = new ArrayList<>(typedTuples.size());
        long minTime = 0;
        int os = 1;
        for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
            // 4.1 获取id
            ids.add(Long.valueOf(tuple.getValue()));
            // 4.2 获取分数(时间戳)
            long time = tuple.getScore().longValue();
            if (time == minTime) {
                os++;
            }else {
                minTime = time;
                os = 1;
            }
        }
                // 5.根据id查询blog
        String idStr = StrUtil.join(",", ids);
        List<Blog> blogs = query().in("id",ids).last("order by field(id," + idStr + ")").list();

        blogs.forEach(blog -> {
            // 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);
    }

6. 附近商户

GEO数据结构

GEO就是Geolocation的简写形式,代表地理坐标。Redis在3.2的版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据,常见的命令有:

  • GEOADD:添加一个地理空间信息,包含:经度、维度、值
  • GEODIST:计算指定的两个点之间的距离并返回
  • GEOHASH:将指定member的坐标转为hash字符串形式并返回
  • GEOPOS:返回指定的member坐标
  • GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回,6.2以后已废弃
  • GEOSEARCH:在指定返回内搜索member,并按照与指定点之间的距离排序后并返回。范围可以是圆形或矩形。6.2新功能
  • GEOSERACHSTORE:与GEOSEARCH的功能一直,不过可以把结果存储到一个指定的key,6.2新功能
    // 查询坐标
    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        // 1.判断是否需要根据坐标查询
        if (x == null || y == null) {
            // 不需要坐标查询,按数据库查
            Page<Shop> page = query().eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            return Result.ok(page.getRecords());
        }

        // 2.计算分页参数
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        // 3.查询redis,按照距离排序,分页。结果:shopId、distance
        String key = "shop:geo:" + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search(
                        key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000), // 5000m
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeCoordinates().limit(end)
                );
        // 4.解析出id
        if (results == null) {
            return Result.ok();
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        if(list.size() <= from) {
            // 没有下一页
            return Result.ok();
        }
        // 截取 from ~ end
        List<Long> ids = new ArrayList<>(list.size());
        Map<String,Distance> distanceMap = new HashMap<>();
        list.stream().skip(from).forEach(result -> {
            // 获取店铺id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            // 获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr,distance);
        });
        // 5.根据id查询shop
        String idStr = StrUtil.join(",", ids);
        List<Shop> shops = query().in("id", ids).last("order by field(id," + idStr + ")").list();
        shops.forEach(shop -> {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        });
        // 6.返回

        return Result.ok(shops);
    }

7. 用户签到

BitMap用法

Redis是利用string类型数据结构实现BitMap,因此最大上限是512M

BitMap的操作命令有:

  • SETBIT:向指定位置 (offset) 存入一个0或1
  • GETBIT:获取指定位置 (offset) 的bit值
  • BITCOUNT:统计BitMap中值为1的bit为的数量
  • BITFIELD:操作 (查询、修改、自增) BitMap 中bit数组中的指定位置 (offset) 的值 (可以批量查询)
  • BITFIELD_RO:获取BitMap中bit数组,并以十进制返回
  • BITOP:将多个BitMap的结果做位运算 (与、或、异或)
  • BITPOS:查找bit数组中指定范围内第一个0或1出现的位置
 // 统计连续签到
    @Override
    public Result signCount() {
        // 1.获取当前登录用户
        Long userId = UserHolder.getUser().getId();
        // 2.获取日期
        LocalDateTime now = LocalDateTime.now();
        // 3.拼接key
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = "sign:" +userId + keySuffix;
        // 4.获取今天是本月的第几天
        int dayOfMonth = now.getDayOfMonth();
        // 5.获取本月截止今天为止的所有签到记录,返回的是一个十进制的数字 BITFIELD key GET u11 0
        List<Long> result = stringRedisTemplate.opsForValue().bitField(
                key,
                BitFieldSubCommands.create()
                        .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
        );
        if (result == null || result.isEmpty()) {
            return Result.ok();
        }
        Long num = result.get(0);
        if (num == null || num == 0) {
            return Result.ok();
        }
        // 6.循环遍历
        int count = 0;
        while (true) {
            // 6.1 让这个数字与1做与运算,得到数字的最后一个bit位,判断这个bit位是否为0  (数与1做与运算等于数本身)
            if ((num & 1) == 0) {
                // 如果0,说明未签到,结束
                break;
            }else {
                // 如果不为0,说明已经签到,计数器+1
                count++;
            }
            // 把数字右移一位,抛弃最后一个bit位,继续下一个bit位
            num >>>= 1;
        }

        return Result.ok(count);
    }

8.UV统计

  • UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问。浏览这个网页的自然人。一天内同一个用户多次访问该网站,只记录一次
  • PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录一次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。

UV统计在服务端做会比较麻烦,因为要判断该用户是否已经统计过了,需要将统计过的用户信息保存。但是如果每个访问的用户都保存在redis中,数据量会非常恐怖。

Hyperloglog(HLL)是从Loglog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。

Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb,内存占用率极低!作为代价,其测量结果是概率性的,有小于0.81%的误差,不对这对于UV统计来说,这完全可以忽略。

模拟UV统计的实现

@Test
void testHyperLoglog() {
    String[] users = new String[];
    int j = 0;
    for(int i = 0;i < 1000000;i++) {
        j = i % 1000;
        users[j] = "user_" + i;
        if(j = 999) {
            // 发送到redis
            stringRedisTemplate.opsForHyperLogLog().add("hll",users);
        }
    }
    // 统计数量
    Long count = stringRedisTemplate.opsForHyperLogLog().size("hll");
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值