沉梦手记 沉梦手记
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 数据库基础
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • 网络通信
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Axios入门
  • Vue Router入门
  • Pinia入门
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 运维与部署
  • 音视频处理
  • 随笔
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
Github (opens new window)

沉梦听雨

时间是最好的浸渍剂,而沉淀是最好的提纯器🚀
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 数据库基础
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • 网络通信
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Axios入门
  • Vue Router入门
  • Pinia入门
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 运维与部署
  • 音视频处理
  • 随笔
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
Github (opens new window)
  • 脚手架搭建
  • 瑞吉外卖

  • 黑马点评

    • Redis实战笔记
    • 登录与缓存优化
    • 优惠券秒杀
    • 分布式锁
    • 秒杀优化与消息队列
    • 社交功能与Feed流
    • GEO与签到统计
      • 9. 怎么实现附近商户功能?
        • 1. 使用 Redis 的 GEO 数据结构存储商铺位置
      • 10. 你是怎么实现用户签到功能的?
        • 签到统计
      • 11. 你是怎么实现 UV 统计?
        • HyperLogLog
    • 面试问答整理
    • 电商场景分析
    • 项目技术亮点
  • vue-blog

  • 沉梦接口开放平台

  • 用户中心

  • 聚合搜索平台

  • 仿12306项目

  • 壁纸小程序项目

  • RuoYi-Vue

  • 项目笔记
  • 黑马点评
沉梦听雨
2023-06-08
目录

GEO与签到统计

# GEO与签到统计

# 9. 怎么实现附近商户功能?

# 1. 使用 Redis 的 GEO 数据结构存储商铺位置

首先,存商家的地址到 Redis。

  • 将数据库中的数据导入到 Redis 中去。GEO 在 Redis 中就是一个 member 和一个经纬度,经纬度对应的就是 tb_shop 中的 x 和 y,而 member 我们用 shop_id 来存。因为 Redis 只是一个内存级数据库,如果存海量的数据还是力不从心,所以我们只存一个 id,用的时候再拿 id 去 SQL 数据库中查询 shop 信息。
  • 但是此时还有一个问题,我们在 Redis 中没有存储 shop_type,无法根据店铺类型来对数据进行筛选。解决办法就是将 type_id 作为 key,存入同一个 GEO 集合即可。
Key Value Score
shop:geo:美食 海底捞 40691512240174598
吉野家 40691519846517915
shop:geo:KTV KTV 01 40691165486458787
KTV 02 40691514154651657

注意:SpringDataRedis 的 2.3.9 版本并不支持 Redis 6.2 提供的 GEOSEARCH 命令,因此我们需要提升其版本,修改自己的 pom.xml 文件。

# 10. 你是怎么实现用户签到功能的?

  • 我使用二进制位来记录每个月的签到情况,签到记录为 1,未签到记录为 0。
  • 把每一个 bit 位对应当月的每一天,形成映射关系,用 0 和 1 标识业务状态,这种思路就称为位图(BitMap)。这样我们就能用极小的空间,来实现大量数据的表示。
  • Redis 中是利用 String 类型数据结构实现 BitMap,因此最大上限是 512M,转换为 bit 则是 2^32 个 bit 位。

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 出现的位置

思路:我们可以把年和月作为 BitMap 的 key,然后保存到一个 BitMap 中。每次签到就把对应位上的 0 变成 1,只要是 1 就说明这一天已经签到了,反之则没有签到。

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

# 签到统计

如何获取本月到今天为止的所有签到数据?

BITFIELD key GET u[dayOfMonth] 0
1

如何从后往前遍历每个 bit 位,获取连续签到天数?

  • 连续签到天数,就是从末尾往前数,看有多少个 1
  • 简单的位运算算法:
int count = 0;
while (true) {
    if ((num & 1) == 0)
        break;
    else
        count++;
    // 数字右移,抛弃最后一位
    num >>>= 1;
}
return count;
1
2
3
4
5
6
7
8
9
10

这里的循环遍历是为了计算签到次数,具体运算流程如下:

  1. 定义一个计数器 count,用来计算签到次数。
  2. 定义一个 long 类型变量 num,并将今天的签到记录存储在其中。
  3. 通过循环遍历,判断签到记录的每一位是否为 1,若为 1 则表示用户当日已签到,签到次数 count 递增。
  4. 由于使用二进制来存储签到历史记录,需要将数字右移,抛弃最后一位。
  5. 当二进制数全部判断完毕后,返回计数器 count,表示当前用户签到的次数。

举个例子,如果 num 的值为 01010101(二进制),则循环遍历每一位标记,可以得出该用户共有 4 次签到,具体计算方法如下:

  1. 初始 count 为 0,num 为 01010101(二进制)。
  2. 遍历 num 的最后一位数字 5(二进制表示为 101),由于最后一位是 1,count 递增 1。
  3. 将 num 右移一位,即变为 00101010,继续判断最后一位数字 2,由于最后一位是 0,不递增 count。
  4. 将 num 右移一位变为 00010101,判断数字 1,由于最后一位是 1,count 递增 1。
  5. 将 num 右移一位变为 00001010,判断数字 0,不递增 count。
  6. 将 num 右移两位变为 00000010,判断数字 2,由于最后一位是 0,不递增 count。
  7. 将 num 右移一位变为 00000001,判断数字 1,由于最后一位是 1,count 递增 1。
  8. 循环完成,最终 count 为 4,即该用户共签到了 4 次。

# 11. 你是怎么实现 UV 统计?

# HyperLogLog

  • UV:全称 Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1 天内同一个用户多次访问该网站,只记录 1 次。
  • PV:全称 Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录 1 次 PV,用户多次打开页面,则记录多次 PV。往往用来衡量网站的流量。
  • 本博客的首页侧边栏就有本站访客量和本站总访问量,对应的就是 UV 和 PV。
  • 通常来说 PV 会比 UV 大很多,所以衡量同一个网站的访问量,我们需要综合考虑很多因素。
  • UV 统计在服务端做会很麻烦,因为要判断该用户是否已经统计过了,需要将统计过的信息保存。但是如果每个访问的用户都保存到 Redis 中,那么数据库会非常恐怖,那么该如何处理呢?
  • HyperLogLog(HLL)是从 Loglog 算法派生的概率算法,用于确定非常大的集合基数,而不需要存储其所有值。算法相关原理可以参考下面这篇文章:https://juejin.cn/post/6844903785744056333#heading-0 (opens new window)
  • Redis 中的 HLL 是基于 string 结构实现的,单个 HLL 的内存永远小于 16kb,内存占用低得令人发指!作为代价,其测量结果是概率性的,有小于 0.81% 的误差。不过对于 UV 统计来说,这完全可以忽略。
上次更新: 2026/10/10 18:05:45
社交功能与Feed流
面试问答整理

← 社交功能与Feed流 面试问答整理→

Theme by Vdoing | Copyright © 2023-2026 沉梦听雨 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式