上一节实现了点赞和关注推送的功能,点赞通过使用Redis中的SortedSet类型完成,关注推送通过Redis模拟消息队列完成基本功能
Q:为什么不使用Set类型?
A:Set集合能实现点赞的基本功能,存储的数据是无序的,让后续对点赞数据进一步操作增加难度。如需要在点赞数据的基础上进行排序
这一节将结合Redis中剩余的数据结构完成附近商户,用户签到,共同关注的功能
附近商户
GEO
GEO(地理信息)是 Redis 中的一个模块,用于存储和处理地理位置信息数据。Redis GEO 模块在3.2版本后引入,提供了一些命令和数据结构,允许你存储地理位置点和查询附近的点
基本命令行
- GEOADD:将一个或多个地理位置点添加到指定的地理位置键中。
- GEOADD key 经度 纬度 序号...[经度 纬度 序号]
- GEOPOS:获取一个或多个地理位置点的经纬度坐标。
- GEOPOS key member [member ...]
- GEODIST:计算两个地理位置点之间的距离。
- GEORADIUS:根据给定的经纬度坐标和半径查找附近的地理位置点。6.2后废弃
- GEORADIUSBYMEMBER:根据指定的地理位置点和半径查找附近的地理位置点。
- GEOHASH:获取一个或多个地理位置点的 Geohash 值。
- GETSEARCH: 指定范围搜索元素,按照距离排序后返回,返回可为矩形,圆形,6.2后新功能
- GEOSEARCH key FROMLONLAT 经度 纬度 BYRADIUS(搜索范围为圆) 距离 距离单位 其他参数
商户入驻
商户入驻包括商户数据的新增,新增过程中需要根据录入地址对接三方平台获取商户的经纬度坐标,然后根据商户进行分类,结合业务存入Redis中
public void saveShopToRedis(){
List<Shop> dataFromDb = ...;
Map<Long,List<Shop>> map = dataFromDb
.stream
.collect(Collectors.groupingBy(Shop::getTypeId));
// 将数据分类分批存入redis中
map.entrySet().for(item->{
Long typeId = item.getKey();
String key = "xxx" + typeId:
List<Shop> value = item.getValue();
List<RedisGeoCommands.GeoLocation<String>> locations =
new ArraysList<>(value.size());
value.forEach(each -> {
locations.add(new RedisGeoCommands.GeoLocation<>{
each.getId().toString(),
new Point(each.getX(),each.getY())
});
stringRedisTemplate.opsforGeo().add(key,locations);
})
})
}
此时当张三打开了你的APP,点击附近商户,只要他允许了手机获取定位权限,那他的位置就会传到后端。什么?你问不给权限?那我也不给你功能咯
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 from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
// 3.查询redis、按照距离排序、分页。结果:shopId、distance
String key = SHOP_GEO_KEY + typeId;
Boolean hasKey = stringRedisTemplate.hasKey(key);
if (!hasKey) {
return Result.ok(Collections.emptyList());
}
// 到redis中查询坐标范围
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
// GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
.search(
key,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
);
// 4.解析出id
if (results == null) {
return Result.ok(Collections.emptyList());
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
if (list.size() <= from) {
// 没有下一页了,结束
return Result.ok(Collections.emptyList());
}
// 4.1.截取 from ~ end的部分
List<Long> ids = new ArrayList<>(list.size());
Map<String, Distance> distanceMap = new HashMap<>(list.size());
list.stream().skip(from).forEach(result -> {
// 4.2.获取店铺id
String shopIdStr = result.getContent().getName();
ids.add(Long.valueOf(shopIdStr));
// 4.3.获取距离
Distance distance = result.getDistance();
distanceMap.put(shopIdStr, distance);
});
// 5.根据id查询Shop
String idStr = StrUtil.join(",", ids);
List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
// 6.返回
return Result.ok(shops);
}
嘿,附近商户数据就有了三,直接在前端分页展示就行了
共同关注
如果你点击了某个人的主页信息,可以在下面加上一个”你的好友‘李四’也关注了对方“,当然,是如果存在的情况下哈
在这之前需要保存关注信息,关注人的信息就可以用Set集合保存,不用关心谁先关注
/*
* @param followUserId 想要关注人的id
* @param isFollow 是否已经关注过
**/
public Result follow(Long followUserId, Boolean isFollow) {
// 1.获取登录用户
Long userId = UserHolder.getUser().getId();
String key = "follows:" + userId;
// 1.判断到底是关注还是取关
if (isFollow) {
// 2.关注,新增数据
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
boolean isSuccess = save(follow);
if (isSuccess) {
// 把关注用户的id,放入redis的set集合 sadd userId followerUserId
stringRedisTemplate.opsForSet().add(key, followUserId.toString());
}
} else {
// 3.取关,删除 delete from tb_follow where user_id = ? and follow_user_id = ?
boolean isSuccess = remove(new QueryWrapper<Follow>()
.eq("user_id", userId).eq("follow_user_id", followUserId));
if (isSuccess) {
// 把关注用户的id从Redis集合中移除
stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
}
}
return Result.ok();
}
现有张三想要查看王五
/*
* @param id 查看用户的id
*
**/
public Result followCommons(Long id) {
// 1.获取当前用户
Long userId = UserHolder.getUser().getId();
String key = "follows:" + userId;
// 2.求交集
String key2 = "follows:" + id;
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
if (intersect == null || intersect.isEmpty()) {
// 无交集
return Result.ok(Collections.emptyList());
}
// 3.解析id集合
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
// 4.查询用户
List<UserDTO> users = userService.listByIds(ids)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(users);
}
嘿,公共关注就有了
用户签到
签到嘛,现在啥都有签到,领取”最终解释权归商家所有“的奖励
方式一:使用数据库保存
其中可能包含的字段有
CREATE TABLE `sign` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT `主键`,
`user_id` bigint(20) unsigned NOT NULL COMMENT `用户id`,
`year` year(4) NOT NULL COMMENT `签到年`,
`month` tinyint(2) NOT NULL COMMENT `签到月`,
`date` date NOT NULL COMMENT `签到日期`,
`is_backup` tinyint(1) unsigned DEFALUT NULL COMMENT `是否补签`,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=uft8mb4;
看标题,感觉上可以搞是吧。但是如果你的项目火了呢,用户量突然暴增,每个人每天都需要向表中插入一条数据,表中数据过大,想想感觉操作起来都不舒适
方式二:使用Redis存储签到
bitMap
BitMap(位图)是一种在计算机中表示和操作数据的数据结构。它由一系列位(0或1)组成,每个位都表示一个特定的信息或状态。通过高效地利用位运算和压缩存储,提供了一种快速、节省空间的数据结构,适用于处理大规模数据集合。
在 Redis 中,BitMap 是通过字符串来实现的。每个字符都可以存储8位(8个位),因此一个长度为N的字符串可以表示N*8个位
基本命令
- SETBIT:设置指定位置的位的值(0或1)。
- GETBIT:获取指定位置的位的值。
- BITCOUNT:统计指定范围内的位为1的个数。
- BITOP:对多个 BitMap 进行位运算,并将结果存储到指定的 BitMap 中。
- BITPOS:查找指定位值(0或1)在指定范围内第一个出现的位置。
- BITFIELD:执行多个位级操作,如设置、获取和修改指定偏移量的位。
结合业务
这里按月进行签到,每个用户签到与否用1,0表示,一个用户一个月下来也只有31bit
正常签到
public void sign(){
// 获取用户信息
Long userId = UserHolder.getUser().getId();
// 获取当前时间
LocalDateTime date = LocalDateTime.now();
String keySuffix = date.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = “xxx” + userId + keySuffix;
int day = now.getDayOfMonth();
// 将日期减一存入
redisTemplate.opsForValue().add(key,day-1,true);
}
统计连续签到
连续签到次数: 从最后一次开始,向前统计,知道遇到第一次未签到位置,计算总的签到次数
获取本月所有签到数据: 一次获取多个bit位,使用BITFIELD,签到时,每天签到数据占一个bit位,所以获取本月签到数量时,取出来的数量为今天的日期BITFIELD key GET u[day] 0
如何从后向前遍历每个bit位: 与 1 做与运算,得到最后一个bit位
public void signCount(){
// 获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 获取日期
LocalDateTime now = LocalDateTime.now();
String format = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
// 拼接key
String key = "user:sign:"+userId+format;
// 获取当前天数
int nowDay = now.getDayOfMonth();
// 从redis中获取数据
List<Long> list = redisTemplate.opsforValue()
.bitField(key,
BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.sign(nowDay))
.valueAt(0));
if(list==null && list.isEmpty()){
return Result.ok(0);
}
Long val = list.get(0);
if(val==null && val == 0){
return Result.ok(0);
}
// 遍历循环
int count = 0;
while(true) {
if((val&1)==1){
count++;
}else{
break;
}
val>>>=1;
}
return Result.ok(count);
}
嘿...
以上就是从一个系统中部分功能衍生出来与Redis进行操作的业务了,如果功能没有达到你的预期,请多多包涵(゜-゜)