GEOSEARCH 是 Redis 6.2 及以上版本引入的一个命令,用于在有序集合(ZSet)中根据地理位置信息进行搜索。它可以基于给定的经纬度坐标和半径范围,查找符合条件的元素,同时还能按距离排序并返回距离信息。
GEOSEARCH key
[FROMLONLAT longitude latitude | FROMMEMBER member]
[BYRADIUS radius m|km|ft|mi | BYBOX width height m|km|ft|mi]
[ASC|DESC]
[COUNT count [ANY]]
[WITHDIST]
[WITHCOORD]
[WITHHASH]
key
:包含地理位置信息的有序集合的键名。
FROMLONLAT longitude latitude
:指定搜索的中心点经纬度。
FROMMEMBER membe
r:指定搜索的中心点为有序集合中的某个成员。
BYRADIUS radius m|km|ft|mi
:以中心点为圆心,指定半径范围进行搜索,单位可以是米(m)、千米(km)、英尺(ft)或英里(mi)。
BYBOX width height m|km|ft|mi
:以中心点为中心,指定矩形区域进行搜索。
ASC|DESC
:指定结果按距离升序或降序排列。
COUNT count [ANY]
:限制返回结果的数量,ANY 表示在找到 count 个结果后立即返回,不继续遍历。
WITHDIST
:返回结果中包含元素与中心点的距离。
WITHCOORD
:返回结果中包含元素的经纬度坐标。
WITHHASH
:返回结果中包含元素的 Geohash 值。
GEORADIUS
是 Redis 中用于基于地理位置信息进行范围查询的命令,它可以找出以给定经纬度为圆心、指定半径范围内的所有地理位置元素。下面详细分析其参数。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count] [STORE key] [STOREDIST key]
key
ZSet
)的形式存储的,每个元素代表一个地理位置,其成员是地理位置的名称,分数是对应的 Geohash 值。shops:geo
表示存储店铺地理位置信息的有序集合。longitude
和 latitude
116.404
和 39.915
表示北京的大致经纬度。radius
2
表示半径为 2 个单位。m|km|ft|mi
m
)、千米(km
)、英尺(ft
)、英里(mi
)。km
表示半径单位为千米。WITHCOORD
GEORADIUS shops:geo 116.404 39.915 2 km WITHCOORD
,返回结果会包含每个店铺的经纬度。WITHDIST
GEORADIUS shops:geo 116.404 39.915 2 km WITHDIST
,返回结果会包含每个店铺与中心点的距离。WITHHASH
GEORADIUS shops:geo 116.404 39.915 2 km WITHHASH
,返回结果会包含每个店铺的 Geohash 值。ASC|DESC
ASC
)或降序(DESC
)排列。GEORADIUS shops:geo 116.404 39.915 2 km WITHDIST ASC
,返回结果按距离中心点由近到远排列。COUNT count
count
是一个正整数。GEORADIUS shops:geo 116.404 39.915 2 km WITHDIST ASC COUNT 5
,只返回距离最近的 5 个店铺。STORE key
GEORADIUS shops:geo 116.404 39.915 2 km STORE nearby_shops
,将符合条件的店铺名称存储到 nearby_shops
有序集合中。STOREDIST key
GEORADIUS shops:geo 116.404 39.915 2 km STOREDIST nearby_shops_with_dist
,将符合条件的店铺名称和距离存储到 nearby_shops_with_dist
有序集合中。在 Spring Data Redis 里,GEORADIUS 命令通过 stringRedisTemplate.opsForGeo().radius() 方法实现,下面详细分析相关参数。
radius() 方法重载形式
主要有两种重载形式:
GeoResults<RedisGeoCommands.GeoLocation<String>> radius(String key, Circle within);
GeoResults<RedisGeoCommands.GeoLocation<String>> radius(String key, Circle within, RedisGeoCommands.GeoRadiusCommandArgs args);
key
含义:存储地理位置信息的有序集合的键名,对应 Redis GEORADIUS 命令中的 key。
示例:
String key = “shops:geo”;
Circle within
含义:定义查询范围,包含中心点和半径。Circle 由 Point 和 Distance 组成,Point 表示中心点经纬度,Distance 表示半径及单位。对应 Redis GEORADIUS 命令中的 longitude、latitude、radius 和 m|km|ft|mi。
@Override
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
// 1.判断是否需要根据坐标查询
if (x == null || y == null) {
// 不需要坐标查询,按数据库查询
// 根据类型分页查询
Page<Shop> page = query()
.eq("type_id", typeId)
.page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
// 返回数据
return Result.ok(page.getRecords());
}
// 2.计算分页参数
int begin = (current - 1) * SystemConstants.MAX_PAGE_SIZE;
int end = begin + SystemConstants.MAX_PAGE_SIZE;
// 3.查询redis、按照距离排序、分页。结果:shopId、distance
String key = SHOP_GEO_KEY + typeId;
// GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
// 构建GEO查询参数
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance()
.sortAscending()
.limit(end);
// 查询店铺信息
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
.radius(key, new Circle(new Point(x, y), new Distance(5000)), args);
// 结果为空返回空集合
if (results == null) {
return Result.ok(Collections.emptyList());
}
// 解析出distance,跳过begin以前
Map<Long,Distance> distanceMap = results.getContent().stream().skip(begin).collect(Collectors.toMap(
result ->Long.valueOf(result.getContent().getName()),
GeoResult::getDistance,
(existing, replacement) -> existing,
LinkedHashMap::new
));
if (distanceMap.size() == 0) {
return Result.ok(Collections.emptyList());
}
// 解析出id
// key
Set<Long> ids = distanceMap.keySet();
String idsStr = StrUtil.join(",", ids);
// 根据id查询店铺
// sql: select * from tb_shop where id in (?, ?, ?) order by field(id, ?, ?, ?)
List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idsStr + ")").list();
// 遍历店铺,设置距离
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId()).getValue());
}
return Result.ok(shops);
}
Bitmap 即位图,在 Redis 里是一种特殊的数据类型,它借助字符串类型来实现位操作,本质上是二进制数组。每个位只能存储 0 或 1,非常适合处理大量的布尔值,如用户签到、在线状态等场景。下面介绍 Redis 中 Bitmap 的基本用法,同时给出 Spring Data Redis 里操作 Bitmap 的示例。
使用 SETBIT
命令可以设置指定偏移量上的位值。
SETBIT key offset value
key
:位图的键名。offset
:位的偏移量,从 0 开始。value
:位的值,只能是 0 或 1。示例:
SETBIT user:sign:1 0 1
上述命令将 user:sign:1
这个位图在偏移量 0 处的值设置为 1。
使用 GETBIT
命令可以获取指定偏移量上的位值。
GETBIT key offset
key
:位图的键名。offset
:位的偏移量,从 0 开始。示例:
GETBIT user:sign:1 0
该命令会返回 user:sign:1
位图在偏移量 0 处的值。
使用 BITCOUNT
命令可以统计位图中值为 1 的位的数量。
BITCOUNT key [start end]
key
:位图的键名。start
和 end
(可选):指定字节范围,用于统计该范围内值为 1 的位的数量。示例:
BITCOUNT user:sign:1
此命令会统计 user:sign:1
位图中值为 1 的位的总数。
使用 BITOP
命令可以对多个位图进行逻辑运算,支持 AND
、OR
、XOR
和 NOT
操作。
BITOP operation destkey key [key ...]
operation
:逻辑运算类型,如 AND
、OR
、XOR
、NOT
。destkey
:存储运算结果的位图键名。key [key ...]
:参与运算的位图键名。示例:
BITOP AND result:and user:sign:1 user:sign:2
该命令对 user:sign:1
和 user:sign:2
两个位图进行逻辑与运算,并将结果存储在 result:and
位图中。
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 设置指定偏移量(offset)的位为 1 或 0
public void setBit(String key, long offset, boolean value) {
redisTemplate.opsForValue().setBit(key, offset, value);
}
// 示例:用户ID=1001在2023-10-01签到(标记为1)
setBit("sign:2023-10:1001", 0, true); // 第0位表示某一天
public Boolean getBit(String key, long offset) {
return redisTemplate.opsForValue().getBit(key, offset);
}
// 示例:检查用户ID=1001在2023-10-01是否签到
Boolean isSigned = getBit("sign:2023-10:1001", 0);
public Long bitCount(String key) {
return redisTemplate.execute(
(RedisCallback<Long>) conn -> conn.bitCount(key.getBytes())
);
}
// 示例:统计用户ID=1001在2023-10月的签到总天数
Long signCount = bitCount("sign:2023-10:1001");
public void bitOp(RedisStringCommands.BitOperation op, String destKey, String... srcKeys) {
redisTemplate.execute(
(RedisCallback<Long>) conn -> conn.bitOp(op, destKey.getBytes(),
Arrays.stream(srcKeys).map(k -> k.getBytes()).toArray(byte[][]::new))
);
}
// 示例:计算两个用户签到记录的交集(AND)
bitOp(RedisStringCommands.BitOperation.AND, "sign:result", "sign:user1", "sign:user2");
通过以上操作,你可以在 Redis 和 Spring Data Redis 中使用 Bitmap 处理布尔值相关的业务场景。
@Override
public Result sign() {
// 获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 当前日期
LocalDateTime now = LocalDateTime.now();
// key
String key = USER_SIGN_KEY + userId + ":" + now.getYear() + ":" + now.getMonth();
// 获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
// 签到,写入redis
stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
return Result.ok();
}
@Override
public Result signCount() {
// 获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 当前日期
LocalDateTime now = LocalDateTime.now();
// key
String key = USER_SIGN_KEY + userId + ":" + now.getYear() + ":" + now.getMonth();
// 获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
// 签到,写入redis
// BITFIELD key u4 0
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;
while(num!=0){
if((num&1)==1){
count++;
}
num>>=1;
}
return Result.ok(count);
}
@Override
public Result signCount() {
// 获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 当前日期
LocalDateTime now = LocalDateTime.now();
// key
String key = USER_SIGN_KEY + userId + ":" + now.getYear() + ":" + now.getMonth();
// 获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
// 签到,写入redis
// BITFIELD key u4 0
Long cnt = stringRedisTemplate.execute(
(RedisCallback<Long>) conn -> conn.bitCount(key.getBytes())
);
return cnt;
}
UV 是 Unique Visitor 的缩写,即独立访客,UV 统计是互联网领域中用于衡量网站、应用程序或特定页面访问量的重要指标,用于统计在一定时间内访问某个站点或应用的不同用户数量。下面从多个方面详细介绍 UV 统计的相关概念。
独立访客指的是在特定时间段内,访问某一网站或应用的不同自然人。同一用户在该时间段内多次访问,仅计算为一个独立访客。比如,在一天内,用户 A 访问了某网站 5 次,用户 B 访问了 3 次,此时该网站当天的 UV 为 2。
在 Spring Data Redis 中使用 HyperLogLog (HLL) 进行 UV 统计,可以通过以下步骤实现。HyperLogLog 提供了一种高效且内存友好的方式来处理大规模独立访客统计,尽管存在约 0.81% 的误差,但适用于大多数场景。
HyperLogLog 是一种概率型数据结构,由 Philippe Flajolet 及其同事在 2007 年提出,2011 年被集成到 Redis 中。它主要用于在牺牲一定精度的前提下,以极小的空间复杂度来统计海量数据的基数(集合中不同元素的个数)。下面从多个方面详细介绍 HyperLogLog。
在互联网场景中,很多时候需要统计独立访客数(UV)、独立 IP 数、搜索关键词数量等,这些数据量可能非常庞大。若使用传统数据结构(如 Set
)来统计,随着数据量增长,内存占用会急剧上升。而 HyperLogLog 能在保证一定精度的情况下,用极少的内存完成基数统计。
HyperLogLog 基于伯努利试验和概率统计原理。简单来说,它把元素通过哈希函数映射为二进制串,记录每个二进制串中从第一个位开始连续 0 的最大个数。根据这些最大 0 个数的统计信息,运用概率公式估算出集合的基数。
PFADD
用于向 HyperLogLog 中添加元素。
PFADD key element [element ...]
key
:HyperLogLog 的键名。element [element ...]
:要添加的元素。示例:
PFADD myhyperloglog user1 user2 user3
PFCOUNT
用于获取 HyperLogLog 中元素的基数估计值。
PFCOUNT key [key ...]
key [key ...]
:要统计的 HyperLogLog 键名,可以指定多个键,此时会返回这些键对应 HyperLogLog 合并后的基数估计值。示例:
PFCOUNT myhyperloglog
PFMERGE
用于将多个 HyperLogLog 合并为一个。
PFMERGE destkey sourcekey [sourcekey ...]
destkey
:合并后的 HyperLogLog 键名。sourcekey [sourcekey ...]
:要合并的 HyperLogLog 键名。示例:
PFMERGE mergedhyperloglog myhyperloglog1 myhyperloglog2
/**
* 向 HyperLogLog 中添加元素
* @param key HyperLogLog 键名
* @param elements 要添加的元素
*/
public void addElements(String key, String... elements) {
stringRedisTemplate.opsForHyperLogLog().add(key, elements);
}
/**
* 获取 HyperLogLog 中元素的基数估计值
* @param keys 要统计的 HyperLogLog 键名
* @return 基数估计值
*/
public Long getCount(String... keys) {
return stringRedisTemplate.opsForHyperLogLog().size(keys);
}
/**
* 合并多个 HyperLogLog
* @param destKey 合并后的 HyperLogLog 键名
* @param sourceKeys 要合并的 HyperLogLog 键名
*/
public void mergeHyperLogLogs(String destKey, String... sourceKeys) {
stringRedisTemplate.opsForHyperLogLog().union(destKey, sourceKeys);
}
测试结果显示997593条数据,HyperLoglog统计存在误差,但是可以极大减少储存空间的消耗同时增加查询的性能。
Set
或 List
,随着数据量的增加,它们的内存占用会线性增长。而 HyperLogLog 能够以恒定的内存来处理海量数据,例如统计每天访问网站的独立用户数,即使访问量达到百万甚至千万级别,内存占用也不会大幅上升。Set
存储每个用户的唯一标识,当用户量达到亿级时,内存占用会非常巨大;而使用 HyperLogLog 仅需 12KB,大大节省了内存资源。