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 或 1GETBIT:获取指定位置(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
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
2
3
4
5
6
7
8
9
10
这里的循环遍历是为了计算签到次数,具体运算流程如下:
- 定义一个计数器 count,用来计算签到次数。
- 定义一个 long 类型变量 num,并将今天的签到记录存储在其中。
- 通过循环遍历,判断签到记录的每一位是否为 1,若为 1 则表示用户当日已签到,签到次数 count 递增。
- 由于使用二进制来存储签到历史记录,需要将数字右移,抛弃最后一位。
- 当二进制数全部判断完毕后,返回计数器 count,表示当前用户签到的次数。
举个例子,如果 num 的值为 01010101(二进制),则循环遍历每一位标记,可以得出该用户共有 4 次签到,具体计算方法如下:
- 初始 count 为 0,num 为
01010101(二进制)。 - 遍历 num 的最后一位数字 5(二进制表示为 101),由于最后一位是 1,count 递增 1。
- 将 num 右移一位,即变为
00101010,继续判断最后一位数字 2,由于最后一位是 0,不递增 count。 - 将 num 右移一位变为
00010101,判断数字 1,由于最后一位是 1,count 递增 1。 - 将 num 右移一位变为
00001010,判断数字 0,不递增 count。 - 将 num 右移两位变为
00000010,判断数字 2,由于最后一位是 0,不递增 count。 - 将 num 右移一位变为
00000001,判断数字 1,由于最后一位是 1,count 递增 1。 - 循环完成,最终 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