一、Redis GEO-实现附近的人
1.1 Redis GEO介绍
Redis GEO主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。
Redis GEO操作方法有:
-
geoadd:添加地理位置的坐标。
-
geopos:获取地理位置的坐标。
-
geodist:计算两个位置之间的距离。
-
georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
-
georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
-
geohash:返回一个或多个位置对象的geohash值。
1.2 georadius与georadiusbymember区别
1、georadius以给定的经纬度为中心,返回键包含的位置元素当中,与中心的距离不超过给定最大距离的所有位置元素。
2、georadiusbymember和georadius命令一样,都可以找出位于指定范围内的元素,但是georadiusbymember的中心点是由给定的位置元素决定的,而不是使用经度和纬度来决定中心点。
georadius与georadiusbymember语法格式如下:
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
参数说明:
-
m:米,默认单位。
-
mi:英里。
-
ft:英尺。
-
WITHDIST:在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
-
WITHCOORD:将位置元素的经度和纬度也一并返回。
-
WITHHASH:以52位有符号整数的形式,返回位置元素经过原始geohash编码的有序集合分值。这个选项主要用于底层应用或者调试,实际中的作用并不大。
-
COUNT:限定返回的记录数。
-
ASC:查找结果根据距离从近到远排序。
-
DESC:查找结果根据从远到近排序。
1.3 Java代码实现
1、初始化GeoOperations
GeoOperations<String, Object> geoOperations = redisTemplate.opsForGeo();
2、记录用户坐标
public boolean saveMemberCoordinate(String memberId,Double longitude,Double latitude) {
Point point = new Point(longitude, latitude));
Long result = geoOperations.add("member:coordinates", point, memberId);
return result!=null ? result > 0 : Boolean.FALSE;
}
3、查用户附近的人
// distance:附近的距离,
// distanceUnit(距离单位):METERS=米(默认单位);KILOMETERS=公里;MILES=英里;FEET=英尺
public List<String> getNearbyMemberIds(String memberId,double distance,RedisGeoCommands.DistanceUnit distanceUnit){
GeoResults<RedisGeoCommands.GeoLocation<Object>> content = geoOperations.geoRadiusByMember("member:coordinates", memberId, new Distance(distance, distanceUnit));
List<String> memberIdList = new ArrayList<>(content.getContent().size());
content.getContent().forEach(item -> {
memberIdList.add(item.getContent().getName().toString());
});
return memberIdList;
}
4、GeoOperations源码
package org.springframework.data.redis.core;
public interface GeoOperations<K, M> {
/**
* Add {@link Point} with given member {@literal name} to {@literal key}.
*
* @param key must not be {@literal null}.
* @param point must not be {@literal null}.
* @param member must not be {@literal null}.
* @return Number of elements added. {@literal null} when used in pipeline / transaction.
* @since 2.0
* @see <a href="https://redis.io/commands/geoadd">Redis Documentation: GEOADD</a>
*/
@Nullable
Long add(K key, Point point, M member);
/**
* Get the {@literal member}s within the circle defined by the {@literal members} coordinates and given
* {@literal radius} applying {@link Metric}.
*
* @param key must not be {@literal null}.
* @param member must not be {@literal null}.
* @param distance must not be {@literal null}.
* @return never {@literal null} unless used in pipeline / transaction.
* @since 2.0
* @see <a href="https://redis.io/commands/georadiusbymember">Redis Documentation: GEORADIUSBYMEMBER</a>
*/
@Nullable
GeoResults<GeoLocation<M>> radius(K key, M member, Distance distance);
/**
* Get the {@literal member}s within the circle defined by the {@literal members} coordinates and given
* {@literal radius} applying {@link Metric}.
*
* @param key must not be {@literal null}.
* @param member must not be {@literal null}.
* @param distance must not be {@literal null}.
* @return never {@literal null} unless used in pipeline / transaction.
* @see <a href="https://redis.io/commands/georadiusbymember">Redis Documentation: GEORADIUSBYMEMBER</a>
* @deprecated since 2.0, use {@link #radius(Object, Object, Distance)}.
*/
@Deprecated
@Nullable
default GeoResults<GeoLocation<M>> geoRadiusByMember(K key, M member, Distance distance) {
return radius(key, member, distance);
}
}
二、zset、hash记录动态点赞
2.1 点赞操作记录
1、点赞/取消点赞参数VO
@Data
@ApiModel(value = "动态点赞VO")
public class DynamicLikeVO {
/** 动态ID */
@ApiModelProperty(value="动态ID",required = true)
String dynamicId;
/** 动态发布者 */
@ApiModelProperty(value="动态发布者")
Long dynamicOwnerId;
@ApiModelProperty(value="是否点赞(true点赞 false 取消点赞)")
Boolean isLike;
}
2、点赞数据存储
public Boolean setLikeOrRemoveDynamic(DynamicLikeVO likeVO, Long memberId) {
List<String> key = new ArrayList<>();
/**
* 1、动态被点赞明细(记录点赞动态的用户id集合)
* 2、key:{qm}dy:l:6130e1620fbe93767755bf41
* 3、数据结构:zset
* 4、数据样例:
* [
* {
* id:1,
* member:157291823300609
* score:1631871153491
* },
* {
* id:1,
* member:148598543155201
* score:1632919179426
* }
* ]
*/
key.add(CacheKeyCombinationConstant.getDynamicLikeCacheKye(likeVO.getDynamicId()));
/**
* 1、动态被点赞总数(记录动态被点赞总数、以及动态其他汇总信息)
* 2、{qm}dy:id:6204725d8fea23424d6b484f
* 3、数据结构:hash
* 4、数据样例:
* {
* likeNums:3,
* readNums:10
* }
*/
key.add(CacheKeyCombinationConstant.getDynamicCacheKey(likeVO.getDynamicId()));
/**
* 1、用户(动态作者)被点赞总数(记录用户被点赞总数、以及其他汇总数据)
* 2、{qm}mem:base:count:178158779432961
* 3、数据结构:hash
* 4、数据样例:
* {
* likeNums:3,
* readNums:10
* }
*/
key.add(CacheKeyCombinationConstant.getMemberBaseCountCacheKey(likeVO.getDynamicOwnerId()));
/**
* 1、用户点赞明细(记录会员主动点赞过的动态id集合)
* 2、{qm}mem:like:190337524301825
* 3、数据结构:zset
* 4、数据样例:
* [
* {
* id:1,
* member:6130e2280fbe93767755ccb8
* score:1631871153491
* },
* {
* id:1,
* member:6130e4b10fbe93767755d2f8
* score:1632919179426
* }
* ]
*/
key.add(CacheKeyCombinationConstant.getMemberLikedCacheKey(memberId));
/**
* 1、用户(动态作者)被点赞明细(记录点赞用户id集合)
* 2、{qm}mem:be:like:157484270551041
* 3、数据结构:zset
* 4、数据样例:
* [
* {
* id:1,
* member:157291823300609
* score:1631871153491
* },
* {
* id:1,
* member:148598543155201
* score:1632919179426
* }
* ]
*/
key.add(CacheKeyCombinationConstant.getMemberBeLikedCacheKey(likeVO.getDynamicOwnerId()));
// 需要用lua脚本保证事务
Object o = this.redisScriptByResource(key, new ClassPathResource("lua/SetLikeOrUnlikeDynamic.lua"), System.currentTimeMillis(), likeVO.getDynamicId(), memberId, likeVO.getIsLike() ? 1 : 0);
Boolean isLike = 0L == Long.parseLong(String.valueOf(o)) ? Boolean.FALSE : Boolean.TRUE;
return isLike;
}
3、SetLikeOrUnlikeDynamic.lua脚本
local dyLikeKey = KEYS[1]
local dyKey = KEYS[2]
local memberKey = KEYS[3]
local memberLikedKey = KEYS[4]
local memberBeLikedKey = KEYS[5]
local score = ARGV[1]
local dyId = ARGV[2]
local member = ARGV[3]
local isLike = tonumber(ARGV[4])
local data = redis.call("zscore", memberLikedKey, dyId)
local beLikedData = redis.call("zscore", memberBeLikedKey, member)
if (isLike ~= 0) then
if(data) then --已经点赞
return 0;
else
if(beLikedData) then --曾经给会员点过赞
redis.call("zincrby", memberBeLikedKey, 1, member)
else
redis.call("zadd", memberBeLikedKey, 1, member) --作者获赞记录 谁给我点过赞
end;
redis.call("zadd",dyLikeKey, score,member) --动态点赞记录
redis.call("zadd",memberLikedKey, score,dyId) --会员点赞记录
redis.call("hincrby", dyKey, "likeNums", 1) --动态获赞数
redis.call("hincrby", memberKey, "likeNums", 1) --作者获赞数
return 1
end;
else
if(data) then
if(beLikedData) then
redis.call("zincrby", memberBeLikedKey, -1, member)
end;
redis.call("zrem",dyLikeKey, member)
redis.call("zrem",memberLikedKey, dyId)
redis.call("hincrby", dyKey, "likeNums", -1)
redis.call("hincrby", memberKey, "likeNums", -1)
return 2
else
return 0
end;
end;
2.2 点赞数据查询
2.2.1 判断用户是否给该动态点过赞
/**
* memberId:用户ID
* dynamicId:动态ID
*/
public boolean userLikedDynamic(Long memberId,String dynamicId){
/**
* 1、用户点赞明细(记录会员主动点赞过的动态id集合)
* 2、{qm}mem:like:190337524301825
* 3、数据结构:zset
* 4、数据样例:
* [
* {
* id:1,
* member:6130e2280fbe93767755ccb8
* score:1631871153491
* },
* {
* id:1,
* member:6130e4b10fbe93767755d2f8
* score:1632919179426
* }
* ]
*/
Double likeNum = this.zScore(CacheKeyCombinationConstant.getMemberLikedCacheKey(memberId), dynamicId);
if(likeNum!=null && likeNum>0){
return true;
}
return false;
}
private ZSetOperations<String, Object> zSetOps;
public Double zScore(String key, Object member) {
return zSetOps.zScore(key, member);
}
2.2.2 获取用户被点赞总数
/**
* 1、用户(动态作者)被点赞总数(记录用户被点赞总数、以及其他汇总数据)
* 2、{qm}mem:base:count:178158779432961
* 3、数据结构:hash
* 4、数据样例:
* {
* likeNums:3,
* readNums:10
* }
*/
public Long getMemberBeLikedCount(Long memberId) {
Object likeNums = hashOps.get(CacheKeyCombinationConstant.getMemberBaseCountCacheKey(memberId), "likeNums");
return null != likeNums ? Long.valueOf(String.valueOf(likeNums)) : 0L;
}
三、list实现App首页动态列表
每个用户在App首页看到的动态列表数据都是不一样的,App首页动态列表数据由两部分组成:
1、根据用户的相关信息(年龄、性别、所在城市、兴趣标签等等)通过推荐算法推荐的动态列表数据。
2、公共的动态列表(用于兜底)
因此,Redis里有两个list集合:
//默认推荐列表list
String DYNAMIC_ANON_REC_DEFAULT = "rec:dy:mem:default";
//给用户推荐的动态列表list
String DYNAMIC_MEMBER_REC = "rec:dy:mem:用户id";
实现逻辑:
1、通过leftPop
取用户推荐列表数据,取完的数据从用户推荐列表里弹出。
2、用户推荐列表里没数据了,去默认推荐列表里通过lrightPopLeftPush
循环取数据,从右边弹出又从左边插入(达到循环效果,App首页动态永远也刷不完)。
核心代码
//取用户推荐列表
List<Object> recIds = redisUtil.lMultiPop(recKey, count);
//取默认推荐列表
List<Object> recIds = redisUtil.lrightPopLeftPush(recKey, recKey, count);
/**
* 右边弹出 左边插入
* @param sourceKey
* @param destinationKey
* @param limit
* @return
*/
public List<Object> lrightPopLeftPush(String sourceKey, String destinationKey, int limit) {
try {
return redisTemplates.executePipelined(new SessionCallback<Object>() {
@Override
public <K, V> Map execute(RedisOperations<K, V> operations) throws DataAccessException {
ListOperations listOperations = operations.opsForList();
for (long i = 0; i < limit; i++) {
listOperations.rightPopAndLeftPush(sourceKey, destinationKey);
}
return null;
}
});
} catch (Exception e) {
e.printStackTrace();
return new ArrayList<>();
} finally {
RedisConnectionUtils.unbindConnection(redisTemplates.getConnectionFactory());
}
}
/**
* 批量弹出
* @param key
* @param limit
* @return
*/
public List<Object> lMultiPop(String key, int limit) {
try {
return redisTemplates.executePipelined(new SessionCallback<Object>() {
@Override
public <K, V> Map execute(RedisOperations<K, V> operations) throws DataAccessException {
ListOperations listOperations = operations.opsForList();
for (long i = 0; i < limit; i++) {
listOperations.leftPop(key);
}
return null;
}
});
} catch (Exception e) {
e.printStackTrace();
return new ArrayList<>();
} finally {
RedisConnectionUtils.unbindConnection(redisTemplates.getConnectionFactory());
}
}
网友评论