Redis入门到入土-章节四-实际业务-附近、签到、共关

143 阅读6分钟

上一节实现了点赞和关注推送的功能,点赞通过使用Redis中的SortedSet类型完成,关注推送通过Redis模拟消息队列完成基本功能

Q:为什么不使用Set类型?

A:Set集合能实现点赞的基本功能,存储的数据是无序的,让后续对点赞数据进一步操作增加难度。如需要在点赞数据的基础上进行排序

这一节将结合Redis中剩余的数据结构完成附近商户用户签到共同关注的功能

附近商户

GEO

GEO(地理信息)是 Redis 中的一个模块,用于存储和处理地理位置信息数据。Redis GEO 模块在3.2版本后引入,提供了一些命令和数据结构,允许你存储地理位置点和查询附近的点

基本命令行

  • GEOADD:将一个或多个地理位置点添加到指定的地理位置键中。
    • GEOADD key 经度 纬度 序号...[经度 纬度 序号]

image.png

  • GEOPOS:获取一个或多个地理位置点的经纬度坐标。
    • GEOPOS key member [member ...]

image.png

  • GEODIST:计算两个地理位置点之间的距离。
  • GEORADIUS:根据给定的经纬度坐标和半径查找附近的地理位置点。6.2后废弃
  • GEORADIUSBYMEMBER:根据指定的地理位置点和半径查找附近的地理位置点。
  • GEOHASH:获取一个或多个地理位置点的 Geohash 值。
  • GETSEARCH: 指定范围搜索元素,按照距离排序后返回,返回可为矩形,圆形,6.2后新功能
    • GEOSEARCH key FROMLONLAT 经度 纬度 BYRADIUS(搜索范围为圆) 距离 距离单位 其他参数

商户入驻

商户入驻包括商户数据的新增,新增过程中需要根据录入地址对接三方平台获取商户的经纬度坐标,然后根据商户进行分类,结合业务存入Redis中

public void saveShopToRedis(){
    List<Shop> dataFromDb = ...;
    Map<Long,List<Shop>> map = dataFromDb
        .stream
        .collect(Collectors.groupingBy(Shop::getTypeId));
    // 将数据分类分批存入redis中
    map.entrySet().for(item->{
        Long typeId = item.getKey();
        String key = "xxx" + typeId:
        List<Shop> value = item.getValue();
        List<RedisGeoCommands.GeoLocation<String>> locations = 
            new ArraysList<>(value.size());
        value.forEach(each -> {
            locations.add(new RedisGeoCommands.GeoLocation<>{
               	each.getId().toString(),
                new Point(each.getX(),each.getY())
            });
            stringRedisTemplate.opsforGeo().add(key,locations);
        })
    })
}

此时当张三打开了你的APP,点击附近商户,只要他允许了手机获取定位权限,那他的位置就会传到后端。什么?你问不给权限?那我也不给你功能咯

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_KEY + typeId;
    Boolean hasKey = stringRedisTemplate.hasKey(key);
    if (!hasKey) {
        return Result.ok(Collections.emptyList());
    }
    // 到redis中查询坐标范围
    GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() 
    // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
            .search(
                    key,
                    GeoReference.fromCoordinate(x, y),
                    new Distance(5000),
                    RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
            );
    // 4.解析出id
    if (results == null) {
        return Result.ok(Collections.emptyList());
    }
    List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
    if (list.size() <= from) {
        // 没有下一页了,结束
        return Result.ok(Collections.emptyList());
    }
    // 4.1.截取 from ~ end的部分
    List<Long> ids = new ArrayList<>(list.size());
    Map<String, Distance> distanceMap = new HashMap<>(list.size());
    list.stream().skip(from).forEach(result -> {
        // 4.2.获取店铺id
        String shopIdStr = result.getContent().getName();
        ids.add(Long.valueOf(shopIdStr));
        // 4.3.获取距离
        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();
    for (Shop shop : shops) {
        shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
    }
    // 6.返回
    return Result.ok(shops);
}

嘿,附近商户数据就有了三,直接在前端分页展示就行了

共同关注

如果你点击了某个人的主页信息,可以在下面加上一个”你的好友‘李四’也关注了对方“,当然,是如果存在的情况下哈

在这之前需要保存关注信息,关注人的信息就可以用Set集合保存,不用关心谁先关注

/*
 * @param followUserId 想要关注人的id
 * @param isFollow 是否已经关注过
 **/
public Result follow(Long followUserId, Boolean isFollow) {
    // 1.获取登录用户
    Long userId = UserHolder.getUser().getId();
    String key = "follows:" + userId;
    // 1.判断到底是关注还是取关
    if (isFollow) {
        // 2.关注,新增数据
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(followUserId);
        boolean isSuccess = save(follow);
        if (isSuccess) {
            // 把关注用户的id,放入redis的set集合 sadd userId followerUserId
            stringRedisTemplate.opsForSet().add(key, followUserId.toString());
        }
    } else {
        // 3.取关,删除 delete from tb_follow where user_id = ? and follow_user_id = ?
        boolean isSuccess = remove(new QueryWrapper<Follow>()
                .eq("user_id", userId).eq("follow_user_id", followUserId));
        if (isSuccess) {
            // 把关注用户的id从Redis集合中移除
            stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
        }
    }
    return Result.ok();
}

现有张三想要查看王五

/*
 * @param id 查看用户的id
 *
 **/
public Result followCommons(Long id) {
    // 1.获取当前用户
    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);
}

嘿,公共关注就有了

用户签到

签到嘛,现在啥都有签到,领取”最终解释权归商家所有“的奖励

方式一:使用数据库保存

其中可能包含的字段有

CREATE TABLE `sign` (
	`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT `主键`,
    `user_id` bigint(20) unsigned NOT NULL COMMENT `用户id`,
    `year` year(4) NOT NULL COMMENT `签到年`,
    `month` tinyint(2) NOT NULL COMMENT `签到月`,
    `date` date  NOT NULL COMMENT `签到日期`,
    `is_backup` tinyint(1) unsigned DEFALUT NULL COMMENT `是否补签`,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=uft8mb4;

看标题,感觉上可以搞是吧。但是如果你的项目火了呢,用户量突然暴增,每个人每天都需要向表中插入一条数据,表中数据过大,想想感觉操作起来都不舒适

方式二:使用Redis存储签到

bitMap

BitMap(位图)是一种在计算机中表示和操作数据的数据结构。它由一系列位(0或1)组成,每个位都表示一个特定的信息或状态。通过高效地利用位运算和压缩存储,提供了一种快速、节省空间的数据结构,适用于处理大规模数据集合。

在 Redis 中,BitMap 是通过字符串来实现的。每个字符都可以存储8位(8个位),因此一个长度为N的字符串可以表示N*8个位

基本命令

  • SETBIT:设置指定位置的位的值(0或1)。
  • GETBIT:获取指定位置的位的值。
  • BITCOUNT:统计指定范围内的位为1的个数。
  • BITOP:对多个 BitMap 进行位运算,并将结果存储到指定的 BitMap 中。
  • BITPOS:查找指定位值(0或1)在指定范围内第一个出现的位置。
  • BITFIELD:执行多个位级操作,如设置、获取和修改指定偏移量的位。

结合业务

这里按月进行签到,每个用户签到与否用1,0表示,一个用户一个月下来也只有31bit

正常签到

public void sign(){
    // 获取用户信息
    Long userId = UserHolder.getUser().getId();
    // 获取当前时间
    LocalDateTime date = LocalDateTime.now();
    String keySuffix = date.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    String key = “xxx” + userId + keySuffix;
    int day = now.getDayOfMonth();
    // 将日期减一存入
    redisTemplate.opsForValue().add(key,day-1,true);
}

统计连续签到

连续签到次数: 从最后一次开始,向前统计,知道遇到第一次未签到位置,计算总的签到次数

获取本月所有签到数据: 一次获取多个bit位,使用BITFIELD,签到时,每天签到数据占一个bit位,所以获取本月签到数量时,取出来的数量为今天的日期BITFIELD key GET u[day] 0

如何从后向前遍历每个bit位: 与 1 做与运算,得到最后一个bit位

public void signCount(){
    // 获取当前登录用户
    Long userId = UserHolder.getUser().getId();
    // 获取日期
    LocalDateTime now = LocalDateTime.now();
    String format = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    // 拼接key
    String key = "user:sign:"+userId+format;
    // 获取当前天数
    int nowDay = now.getDayOfMonth();
    // 从redis中获取数据
    List<Long> list = redisTemplate.opsforValue()
        .bitField(key,
                 BitFieldSubCommands.create()
                 .get(BitFieldSubCommands.BitFieldType.sign(nowDay))
                 .valueAt(0));
    if(list==null && list.isEmpty()){
        return Result.ok(0);
    }
    Long val = list.get(0);
    if(val==null && val == 0){
        return Result.ok(0); 
    }
    // 遍历循环
    int count = 0;
    while(true) {
        if((val&1)==1){
            count++;
        }else{
            break;
        }
        val>>>=1;
    }
    return Result.ok(count);
}

嘿...

秒杀业务

以上就是从一个系统中部分功能衍生出来与Redis进行操作的业务了,如果功能没有达到你的预期,请多多包涵(゜-゜)