• Redis实战案例及问题分析之-附近商铺(GEO数据结构)-用户签到(BitMap)-UV统计(HyperLogLog)


    附近商铺

    GEO数据结构

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

    GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member

    GEODIST:计算指定的两个点之间的距离并返回

    GEOHASH:将指定member的坐标转为hash字符串形式并返回

    GEOPOS:返回指定member的坐标

    GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6.2以后已废弃

    GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能

    GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key 6.2.新功能

    附近商铺搜索

    在首页中点击某个频道,即可看到频道下的商户:

     导入商铺的id信息以及地理信息

    1. @Test
    2. void loadShopData(){
    3. //1.查询店铺信息
    4. List<Shop> list = shopService.list();
    5. //2.把店铺分组,按照typeid,一致的放到一个集合
    6. Map<Long,List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
    7. //3.分批完成存储写入redis
    8. for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
    9. //3.1获取类型id
    10. Long typeId = entry.getKey();
    11. String key = SHOP_GEO_KEY + typeId;
    12. //3.2获取同类型的店铺集合
    13. List<Shop> value = entry.getValue();
    14. List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
    15. //3.写入redis
    16. for (Shop shop : value) {
    17. //stringRedisTemplate.opsForGeo().add(key,new Point(shop.getX(),shop.getY()), shop.getId().toString());
    18. locations.add(new RedisGeoCommands
    19. .GeoLocation<>(shop.getId()
    20. .toString(), new Point(shop.getX(),shop.getY())));
    21. }
    22. stringRedisTemplate.opsForGeo().add(key,locations);
    23. }
    24. }

    实现根据距离查店铺、分页展示

    1. @Override
    2. public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
    3. //1.判断是否需要根据坐标查询
    4. if (x == null || y == null){
    5. //不需要坐标查询,按数据库查询
    6. Page<Shop> page = query()
    7. .eq("type_id", typeId)
    8. .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
    9. //返回数据
    10. return Result.ok(page.getRecords());
    11. }
    12. //2.计算分页参数
    13. int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
    14. int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
    15. //3.查询reis,按照距离排序、分页。结果:shopid、distance
    16. String key = SHOP_GEO_KEY + typeId;
    17. GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
    18. .search(
    19. key,
    20. GeoReference.fromCoordinate(x, y),
    21. new Distance(5000),
    22. RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));
    23. //4.解析id
    24. if (results == null){
    25. return Result.ok(Collections.emptyList());
    26. }
    27. List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
    28. if (list.size() <= from) {
    29. //没有下一条了
    30. return Result.ok(Collections.emptyList());
    31. }
    32. //4.1截取from - end 部分
    33. List<Long> ids = new ArrayList<>(list.size());
    34. Map<String , Distance> distanceMap = new HashMap<>(list.size());
    35. list.stream().skip(from).forEach(result ->{
    36. //4.2获取店铺ID
    37. String shopIdStr = result.getContent().getName();
    38. ids.add(Long.valueOf(shopIdStr));
    39. //4.3获取店铺距离
    40. Distance distance = result.getDistance();
    41. distanceMap.put(shopIdStr,distance);
    42. });
    43. //5.根据id查询shop
    44. String idStr = StrUtil.join(",", ids);
    45. List<Shop> shops = query()
    46. .in("id", ids)
    47. .last("ORDER BY FIELD(id," + idStr + ")")
    48. .list();
    49. for (Shop shop : shops) {
    50. shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
    51. }
    52. //6.返回
    53. return Result.ok(shops);
    54. }

    用户签到

    我们按月来统计用户签到信息,签到记录为1,未签到则记录为0

    把每一个bit位对应当月的每一天,形成了映射关系。用01标示业务状态,这种思路就称为位图(BitMap

    Redis是利用string类型数据结构实现BitMap因此最大上限是512M,转换为bit则是 2^32bit位。

    BitMap的操作命令有:

    SETBIT:向指定位置(offset)存入一个01

    GETBIT :获取指定位置(offset)的bit

    BITCOUNT :统计BitMap中值为1bit位的数量

    BITFIELD :操作(查询、修改、自增)BitMapbit数组中的指定位置(offset)的值

    BITFIELD_RO :获取BitMapbit数组,并以十进制形式返回

    BITOP :将多个BitMap的结果做位运算(与 、或、异或)

    BITPOS :查找bit数组中指定范围内第一个01出现的位置

    需求:实现签到接口,将当前用户当天签到信息保存到Redis中

    1. public Result sign() {
    2. //1.获取当前登录用户
    3. Long userId = UserHolder.getUser().getId();
    4. //2.获取日期
    5. LocalDateTime now = LocalDateTime.now();
    6. //3.拼接key
    7. String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    8. String key = USER_SIGN_KEY + userId + keySuffix;
    9. //4.获取今天是本月的第几天
    10. int dayOfMonth = now.getDayOfMonth();
    11. //5.写入redis
    12. stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
    13. return Result.ok();
    14. }

     签到统计

    问题1:什么叫做连续签到天数?

    从最后一次签到开始向前统计,直到遇到第一次未签到为止,计算总的签到次数,就是连续签到天数。

    问题2:如何得到本月到今天为止的所有签到数据?

      BITFIELD key GET u[dayOfMonth] 0

    问题3:如何从后向前遍历每个bit位?

    1 做与运算,就能得到最后一个bit位。

    随后右移1位,下一个bit位就成为了最后一个bit位。

    需求:实现下面接口,统计当前用户截止当前时间在本月的连续签到天数

     

    1. public Result signCount() {
    2. //1.获取当前登录用户
    3. Long userId = UserHolder.getUser().getId();
    4. //2.获取日期
    5. LocalDateTime now = LocalDateTime.now();
    6. //3.拼接key
    7. String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    8. String key = USER_SIGN_KEY + userId + keySuffix;
    9. //4.获取今天是本月的第几天
    10. int dayOfMonth = now.getDayOfMonth();
    11. //5.获取本月截止今天为止所有的签到记录
    12. List<Long> results = stringRedisTemplate.opsForValue().bitField(
    13. key, BitFieldSubCommands.create()
    14. .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
    15. );
    16. if (results == null || results.isEmpty()){
    17. //没有任何签到结果
    18. return Result.ok(0);
    19. }
    20. Long num = results.get(0);
    21. if (num == null || num == 0){
    22. //没有任何签到结果
    23. return Result.ok(0);
    24. }
    25. //6.循环遍历
    26. int count = 0;
    27. while (true){
    28. //7.1让这个数字与1做与运算,得到数字的最后一个bit位,判断这个bit是否为0
    29. if ((num & 1) == 0){
    30. //7.2如果为0,说明未签到,循环结束
    31. break;
    32. }else {
    33. //7.3如果不为零,说明已签到,计数器+1
    34. count++;
    35. }
    36. //7.4把数字右移一位,把最后一个bit抛弃,继续统计下一个Bit
    37. num >>>= 1;
    38. }
    39. return Result.ok(count);
    40. }

    UV统计

    首先搞懂两个概念:

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

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

    HyperLogLog用法

    Hyperloglog(HLL)是从Loglog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。相关算法原理大家可以参考:https://juejin.cn/post/6844903785744056333#heading-0

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

  • 相关阅读:
    【昇思MindSpore】MindSpore的安装
    python从入门到出家(五)循环语句
    Day16--购物车页面-商品列表-基于props封装radio的勾选状态
    警方打击了大规模网络钓鱼提供商BulletProftLink
    mysql 基于GTID方式的bin-log日志恢复数据
    Remove和RemoveLast用法
    Facebook元宇宙大观:数字化社交的未来愿景
    CloudQuery + StarRocks:打造高效、安全的数据库管控新模式
    关于将对象转成JSON格式的一些问题
    【大数据入门核心技术-Hadoop】(三)Hadoop基础概念之MapReduce
  • 原文地址:https://blog.csdn.net/PnJgHT/article/details/125554250