【Redis应用】基于redis实现定位,签到,统计等功能
Redis除了做缓存,还能做什么?
前言
我们上次讲了基于redis实现点赞,关注,滚动分页的功能,这些功能都依赖于redis可支持数据结构的特点。在本章中我进行学习了redis的其他功能,主要就是依赖于redis另外的几种数据结构:BitMap:可实现签到,HyperLogLog实现统计,以及定位功能也是依赖于redis中的GEO。
基于redis实现附近搜索功能
Redis的GEO数据结构前置知识
Redis的GEO(地理)功能也支持地理坐标搜索。Redis是一种内存数据库,提供了对地理位置数据的支持。通过使用Redis的GEO命令,你可以将地理位置(经度和纬度)与指定的地点或对象关联起来,并执行各种地理坐标搜索操作。
以下是Redis GEO功能的一些常见命令:
- GEOADD:将给定的地理位置与指定的地点或对象关联起来。
- GEODIST:计算两个地理位置之间的距离。
- GEOHASH:获取指定地理位置的geohash值。
- GEOPOS:获取一个或多个地点的经度和纬度。
- GEORADIUS:根据给定的地理坐标和半径,在指定的范围内搜索地点。(6.2废弃)
- GEOSEARCH:在指定范围内搜索member,并按照于指定点之间的距离排序后返回。范围可以是矩形或圆形(6.2新功能)
- GEORADIUSBYMEMBER:根据给定的地点,搜索在指定的范围内的其他地点。
- 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集合中即可
key | value | score |
---|---|---|
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用法
使用数据库存储签到信息的缺点:
- 存储空间占用:数据库表在存储签到数据时可能占用更多的存储空间,因为它需要存储更多的字段和元数据信息。
- 数据操作性能:数据库表的读写操作通常会更慢一些,特别是在处理大规模签到数据时。数据库的索引和查询机制可能会引入一定的延迟和开销。
为解决上面的数据库用来存储签到带来的缺点,可以想想在大量签到数据面前,然后保证数据读写快速的同时,节约内存空间? -- 我们可以想到redis中的bitmap数据结构,下面是bitmap的优点:
- 空间效率高:Bitmap 使用二进制位来表示状态,每个位只占用一个 bit,因此在存储上非常紧凑。对于大规模用户签到数据,使用 Bitmap 能够显著减少存储空间的占用。(一个用户一个月的签到数据只占31bit)
- 快速计算:Redis 提供了一系列针对 Bitmap 的位操作命令,如设置位、清除位、查询位等,这些操作都能在常数时间内完成。这使得签到的数据操作非常高效,无论是进行签到记录还是查询连续签到天数都可以快速完成。
- 支持统计功能:Redis 的 Bitmap 提供了强大的位操作命令,可以方便地对位图进行统计操作。例如,可以轻松计算总签到次数、计算连续签到天数、计算累计签到天数等统计指标。
- 灵活性:Bitmap 可以表示任意的二进制状态,不仅仅局限于签到功能。这意味着你可以在同一个 Bitmap 中存储其他类型的状态信息,如用户活跃状态、任务完成状态等,从而扩展其它功能。
- SETBIT key offset value:设置指定键的偏移量上的位的值。偏移量从左到右,最左边的位的偏移量为 0。value 只能是 0 或 1。
- GETBIT key offset:获取指定键的偏移量上位的值。
- BITCOUNT key [start end]:计算指定键中指定范围内的位值为 1 的个数。如果未提供范围,则计算整个位图的个数。
- BITOP operation destkey key [key ...]:对一个或多个位图进行指定的位运算,并将结果保存到目标键中。操作可以是 AND、OR、XOR、NOT。
- BITPOS key bit [start] [end]:查找指定键中从 start 到 end 范围内,第一个等于指定位值(bit)的位的偏移量。
- BITFIELD key [GET type offset] [SET type offset value]:对指定键的位图进行多种位操作,可以进行位的读取和设置。这个命令比较灵活,可以用于针对位图进行更复杂的操作。
- 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.往往用来衡量网站的浏览量.
- 添加元素:使用 PFADD 命令向 HyperLogLog 中添加元素。命令的语法如下:
PFADD key element [element ...]
- 其中,key 是 HyperLogLog 的键,element 是要添加的元素。你可以一次添加一个或多个元素。
- 统计基数:使用 PFCOUNT 命令统计 HyperLogLog 中的基数。命令的语法如下:
PFCOUNT key [key ...]
- 可以同时统计一个或多个 HyperLogLog 的基数。
- 合并 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);
}
转载自:https://juejin.cn/post/7352075755822891034