likes
comments
collection
share

【Redis应用】基于redis实现定位,签到,统计等功能

作者站长头像
站长
· 阅读数 8

Redis除了做缓存,还能做什么?

前言

我们上次讲了基于redis实现点赞,关注,滚动分页的功能,这些功能都依赖于redis可支持数据结构的特点。在本章中我进行学习了redis的其他功能,主要就是依赖于redis另外的几种数据结构:BitMap:可实现签到,HyperLogLog实现统计,以及定位功能也是依赖于redis中的GEO。

基于redis实现附近搜索功能

Redis的GEO数据结构前置知识

Redis的GEO(地理)功能也支持地理坐标搜索。Redis是一种内存数据库,提供了对地理位置数据的支持。通过使用Redis的GEO命令,你可以将地理位置(经度和纬度)与指定的地点或对象关联起来,并执行各种地理坐标搜索操作。

以下是Redis GEO功能的一些常见命令:

  1. GEOADD:将给定的地理位置与指定的地点或对象关联起来。
  2. GEODIST:计算两个地理位置之间的距离。
  3. GEOHASH:获取指定地理位置的geohash值。
  4. GEOPOS:获取一个或多个地点的经度和纬度。
  5. GEORADIUS:根据给定的地理坐标和半径,在指定的范围内搜索地点。(6.2废弃)
  6. GEOSEARCH:在指定范围内搜索member,并按照于指定点之间的距离排序后返回。范围可以是矩形或圆形(6.2新功能)
  7. GEORADIUSBYMEMBER:根据给定的地点,搜索在指定的范围内的其他地点。
  8. GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。

练习:

  • 添加数据 GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]

    • 北京南站(116.378248 39.865275)
    • 北京站(116.42803 39.903738)
    • 北京西站(116.322287 39.893729)

geoadd test:geodemo 116.378248 39.865275 bjn 116.42803 39.903738 bj 116.322287 39.893729 bjx

  • 计算北京西站与北京的距离:GEODIST key member1 member2 [unit单位]

geodist test:geodemo bjx bj

  • 搜索天安门(116.397904 39.909005)附近10km的所有火车站,并按照距离升序排序

整合java实现

接口设计

说明
请求方式GET
请求路径/shop/of/type
请求参数typeId:商户类型;current:页码,滚动查询;x:经度;y:维度
返回值List:符合要求的商户信息

需求:

按照商户类型做分组,类型相同的商户作为同一组,以typeId为key存入同一个GEO集合中即可

keyvaluescore
shop:geo:美食商店1时间戳
商店2时间戳
shop:geo:KTV商店3时间戳
商店4时间戳

代码实现

@Override
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
    // 1.判断是否需要根据坐标查询
    if (x == null || y == null){
        Page<Shop> page = lambdaQuery().eq(Shop::getTypeId, 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
    GeoResults<RedisGeoCommands.GeoLocation<String>> search = stringRedisTemplate.opsForGeo().search(
            RedisConstants.SHOP_GEO_KEY + typeId,
            GeoReference.fromCoordinate(x, y),
            new Distance(5000),
            RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
    );
    // 4.解析出id
    if (search == null){
        return Result.ok(Collections.emptyList());
    }
    List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = search.getContent();
    // 4.1 截取from ~ end 的部分
    List<Long> ids = new ArrayList<>(content.size());
    Map<String, Distance> distanceMap = new HashMap<>(content.size());
    content.stream().skip(from).forEach(res -> {
        // 获取店铺id
        String shopIdStr = res.getContent().getName();
        ids.add(Long.valueOf(shopIdStr));
        // 获取距离
        Distance distance = res.getDistance();
        distanceMap.put(shopIdStr,distance);
    });
    // 5.根据id查询
    List<Shop> shops = new ArrayList<>();
    List<Shop> shopList = ids.stream().map(id -> {
        Shop shop = getById(id);
        shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        shops.add(shop);
        return shop;
    }).toList();
    return Result.ok(shopList);
}

用户签到实现

BitMap用法

使用数据库存储签到信息的缺点:

  1. 存储空间占用:数据库表在存储签到数据时可能占用更多的存储空间,因为它需要存储更多的字段和元数据信息。
  2. 数据操作性能:数据库表的读写操作通常会更慢一些,特别是在处理大规模签到数据时。数据库的索引和查询机制可能会引入一定的延迟和开销。

为解决上面的数据库用来存储签到带来的缺点,可以想想在大量签到数据面前,然后保证数据读写快速的同时,节约内存空间? -- 我们可以想到redis中的bitmap数据结构,下面是bitmap的优点:

  1. 空间效率高:Bitmap 使用二进制位来表示状态,每个位只占用一个 bit,因此在存储上非常紧凑。对于大规模用户签到数据,使用 Bitmap 能够显著减少存储空间的占用。(一个用户一个月的签到数据只占31bit)
  2. 快速计算:Redis 提供了一系列针对 Bitmap 的位操作命令,如设置位、清除位、查询位等,这些操作都能在常数时间内完成。这使得签到的数据操作非常高效,无论是进行签到记录还是查询连续签到天数都可以快速完成。
  3. 支持统计功能:Redis 的 Bitmap 提供了强大的位操作命令,可以方便地对位图进行统计操作。例如,可以轻松计算总签到次数、计算连续签到天数、计算累计签到天数等统计指标。
  4. 灵活性:Bitmap 可以表示任意的二进制状态,不仅仅局限于签到功能。这意味着你可以在同一个 Bitmap 中存储其他类型的状态信息,如用户活跃状态、任务完成状态等,从而扩展其它功能。
  1. SETBIT key offset value:设置指定键的偏移量上的位的值。偏移量从左到右,最左边的位的偏移量为 0。value 只能是 0 或 1。
  2. GETBIT key offset:获取指定键的偏移量上位的值。
  3. BITCOUNT key [start end]:计算指定键中指定范围内的位值为 1 的个数。如果未提供范围,则计算整个位图的个数。
  4. BITOP operation destkey key [key ...]:对一个或多个位图进行指定的位运算,并将结果保存到目标键中。操作可以是 AND、OR、XOR、NOT。
  5. BITPOS key bit [start] [end]:查找指定键中从 start 到 end 范围内,第一个等于指定位值(bit)的位的偏移量。
  6. BITFIELD key [GET type offset] [SET type offset value]:对指定键的位图进行多种位操作,可以进行位的读取和设置。这个命令比较灵活,可以用于针对位图进行更复杂的操作。
  7. BITFIELD_RO: 获取bitmap中的bit数组,并以十进制形式返回

签到功能的实现

接口设计

说明
请求方式Post
请求路径/user/sign
请求参数
返回值

controller层

@PostMapping("/sign")
public Result sign(){
    return userService.sign();
}

service层

@Override
public Result sign() {
    // 1.获取当前登录用户
    UserDTO user = UserHolder.getUser();
    if (user == null) {
        return Result.fail(SystemConstants.USER_UN_LOGIN);
    }
    // 2.获取日期
    LocalDateTime now = LocalDateTime.now();
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    // 3.拼接key
    String key = RedisConstants.USER_SIGN_KEY + user.getId() + keySuffix;
    // 4.获取今日是本月的第几天
    int dayOfMonth = now.getDayOfMonth();
    // 5.写入Redis
    stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
    return Result.ok();
}

签到统计

连续签到:从最新一天开始往前算,当遇到第一个0的时候截止.

说明
请求方式GET
请求路径/user/sign/count
请求参数
返回值连续签到天数

controller层

@GetMapping("/sign/count")
public Result signCount() {
    return userService.signCount();
}

service层

@Override
public Result signCount() {
    // 1.获取当前登录用户
    UserDTO user = UserHolder.getUser();
    if (user == null) {
        return Result.fail(SystemConstants.USER_UN_LOGIN);
    }
    Long userId = user.getId();
    // 2.获取日期
    LocalDateTime now = LocalDateTime.now();
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    // 3.拼接key
    String key = RedisConstants.USER_SIGN_KEY + userId + keySuffix;
    // 4.获取今日是本月的第几天
    int dayOfMonth = now.getDayOfMonth();
    // 5.获取本月截至今天为止的所以签到记录,返回一个十进制数字
    List<Long> result = stringRedisTemplate.opsForValue().bitField(key,
            BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
    if (result == null || result.isEmpty()) {
        // 没有如何签到结果
        return Result.ok(0);
    }
    Long num = result.get(0);
    if (num == null || num == 0) {
        return Result.ok(0);
    }
    int count = 0;
    // 6.循环遍历
    while (true){
        // 7.让这个数据与1做与运算,得到数字的最后一个bit位 // 8.判断这个bit位是否为0
        if ((num & 1) == 0){
            // 9.如果为0,说明未签到,结束
            break;
        }else {
            // 10.如果不为0,说明已签到,计数器+1;   
            count++;
        }
        // 把数据右移一位,抛弃最后一个bit位,继续下一个bit位
        num >>>= 1;
    }
    return Result.ok(count);
}

UV统计

HyperLogLog的用法

  • UV: 全称Unique Visitor,也叫独立访客量,是指通过互联网访问,浏览这个网页的自然人.1天内同一个用户多次访问该网站,只记录1次
  • PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV.往往用来衡量网站的浏览量.
  1. 添加元素:使用 PFADD 命令向 HyperLogLog 中添加元素。命令的语法如下:
PFADD key element [element ...]
  1. 其中,key 是 HyperLogLog 的键,element 是要添加的元素。你可以一次添加一个或多个元素。
  2. 统计基数:使用 PFCOUNT 命令统计 HyperLogLog 中的基数。命令的语法如下:
PFCOUNT key [key ...]
  1. 可以同时统计一个或多个 HyperLogLog 的基数。
  2. 合并 HyperLogLog:使用 PFMERGE 命令将多个 HyperLogLog 合并为一个。命令的语法如下:
PFMERGE destkey sourcekey [sourcekey ...]

实现UV统计

@Test
void testHyperLogLog() {
    String[] user = new String[1000];
    int j = 0;
    for (int i=0;i<1000000;i++) {
        j = i % 1000;
        user[j] = "user_" + i;
        if (j == 999){
            stringRedisTemplate.opsForHyperLogLog().add("test:hyper",user);
        }
    }
    // 统计数量
    Long count = stringRedisTemplate.opsForHyperLogLog().size("test:hyper");
    System.out.println("count = "+count);
}