1、Redis有序集的使用
不管是排行榜、关注他人、取关他人或好友推荐等功能的实现,背后还是利用Redis的有序数列集合(zset)及相关命令来实现。
先回顾排行榜实现原理:
有序集合由三部分组成,KEY(键)、score(成员的得分)、member(成员)。有序集合的每一项,都是以键值对的形式存储,每一项都有一个分数。有序集合会根据score自动排序。利用这个特性,就可实现排行榜。
score 是数字类型,可以是整形也可以是浮点型。相同分值的情况下,redis是按照 member 的 ASCII码进行排序。
抛出一个小疑问:
假如排行榜的评分标准不仅仅单比较一个属性,而是多属性的比较时(如:相同分值下再按照先来后到的顺序排序),应该怎么办?
策略:对评分过程做细节处理,将 score进行改造,同时记录得分与时间信息。实现方式有以下两种:
- 重新拼接一个得分 + 时间差的整数score。
- 重新拼接一个得分 + 时间差的浮点数score。整数部分使用得分,小数部分使用时间差。注:1、浮点数时,小数点前后位数和不要超过16位,最好15位。超过16位后,score值存入redis,会发生精度丢失。2、时间差由unix时间戳生成
redis有序集zset常用命令:
//获取整个排行榜,分值递减排列,跟ZRANGE作用相反
ZREVRANGE yourkey 0 -1 withscores
//获取排行榜前n名,分值递减排列
ZREVRANGE yourkey 0 n-1 withscores
//添加一个或多个成员到有序集合,或者如果它已经存在更新其分数
ZADD key score1 member1 [score2 member2]
//由索引返回一个成员范围的有序集合(分值递增)。
ZRANGE key start stop [WITHSCORES]
//获取给定有序集合中的成员及相关联的分值
ZSCORE key member
//获取成员的排名
ZRANK key member
//移除某个成员
ZREM key member
//统计集合内的成员数
ZCARD key
//返回指定范围元素的个数,key后面加(min max)or (min max or min max) or min max 可设置开闭区间
ZCOUNT key min max
//获取两个或多个集合的交集,时间复杂度: O(N * M), N 为给定集合当中基数最小的集合, M 为给定集合的个数。
ZINTER key1 key2 [key3···]
//简单使用(将key1和key2的交集存储于key3):
ZINTERSTORE key3 key1 key2
//取两个或多个集合的交集,复杂使用:
//numkeys指定key的数量,必须
//WEIGHTS选项,与前面设定的key对应,对应key中每一个score都要乘以这个权重
//AGGREGATE选项,指定交集的聚合方式
//SUM:将所有集合中某一个元素的score值之和作为结果集中该成员的score值
//MIN:将所有集合中某一个元素的score值中最小值作为结果集中该成员的score值
//MAX:将所有集合中某一个元素score值中最大值作为结果集中该成员的score值
ZINTERSTORE destination numkeys key [key …][WEIGHTS weight [weight …]] [AGGREGATE SUM|MIN|MAX]
2、简单实现好友关系设置
-
分析
现在很多社交都有关注或取关等功能,若仅仅是获取用户的一些粉丝或所关注的人,可采用传统数据库实现类似功能,实现较容易, 但若是想要查询出两个、甚至多个用户共同关注了哪些人,或者想要查询两个或多个用户的共同粉丝的话就会很麻烦,效率也不会很高。但是如果用redis去做的话,得益于redis封装了多个集合的交集、并集、差集等操作。实现起来就会相当简单高效。只需为每个用户设置两个集合即可,fans_userID集合表示用户拥有的粉丝集合,following_userID集合表示用户所关注的人结合。后续操作如下: -
核心逻辑代码如下:
首先添加相关依赖,再定义一个简易redis工具类
/**
* @author zhanghuigen@cetiti.com
* @since 0.1.0
**/
public final class RedisUtil {
private static String ADDR = "localhost";
private static int PORT = 6379;
private static String AUTH = "admin";
//控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值8。
private static int MAX_IDLE = 200;
//超时时间
private static int TIMEOUT = 10000;
//是否提前进行validate操作
private static boolean TEST_ON_BORROW = true;
private static JedisPool jedisPool = null;
static {
try {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(MAX_IDLE);
config.setTestOnBorrow(TEST_ON_BORROW);
jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT, AUTH);
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized static Jedis getJedis() {
try {
if (jedisPool != null) {
return jedisPool.getResource();
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@SuppressWarnings("deprecation")
public static void returnResource(final Jedis jedis) {
if (jedis != null) {
jedisPool.returnResource(jedis);
}
}
}
封装简单的关注工具类FollowUtil,
根据redis相关命令实现关注、获取粉丝、获取共同好友等功能,详见注释:
/**
* @author zhanghuigen@cetiti.com
* @since 0.1.0
**/
public class FollowUtil {
private static final String FOLLOWING = "FOLLOWING_";
private static final String FANS = "FANS_";
private static final String COMMON_KEY = "COMMON_FOLLOWING_";
/**
* 关注或者取消关注
*/
public static int addOrRelease(String userId, String followingId) {
if (userId == null || followingId == null) {
return -1;
}
// 0 = 取消关注 1 = 关注
int isFollow = 0;
Jedis jedis = RedisUtil.getJedis();
String followingKey = FOLLOWING + userId;
String fansKey = FANS + followingId;
if (jedis.zrank(followingKey, followingId) == null) {
// 说明userId没有关注过followingId
jedis.zadd(followingKey, System.currentTimeMillis(), followingId);
jedis.zadd(fansKey, System.currentTimeMillis(), userId);
isFollow = 1;
} else {
// 取消关注
jedis.zrem(followingKey, followingId);
jedis.zrem(fansKey, userId);
}
return isFollow;
}
/**
* 验证两个用户之间的关系
* 0: 没关系
* 1: 自己
* 2: userId关注了otherUserId
* 3: otherUserId是userId的粉丝
* 4: 互相关注
* @param userId userId
* @param otherUserId otherUserId
* @return int
*/
public int checkRelations (String userId, String otherUserId) {
if (userId == null || otherUserId == null) {
return 0;
}
if (userId.equals(otherUserId)) {
return 1;
}
Jedis jedis = RedisUtil.getJedis();
String followingKey = FOLLOWING + userId;
int relation = 0;
// userId是否关注otherUserId
if (jedis.zrank(followingKey, otherUserId) != null) {
relation = 2;
}
String fansKey = FANS + userId;
// userId粉丝列表中是否有otherUserId
if (jedis.zrank(fansKey, userId) != null) {
relation = 3;
}
if ((jedis.zrank(followingKey, otherUserId) != null)
&& jedis.zrank(fansKey, userId) != null) {
relation = 4;
}
return relation;
}
/**
* 获取用户所有关注的人
* @param userId userId
* @return set
*/
public static Set<String> findFollowings(String userId) {
return findSet(FOLLOWING + userId);
}
/**
* 获取用户所有粉丝
* @param userId userId
* @return set
*/
public static Set<String> findFans(String userId) {
return findSet(FANS + userId);
}
/**
* 获取两个共同关注的人
* @param userId userId
* @param otherUserId otherUserId
* @return set
*/
public static Set<String> findCommonFollowing(String userId, String otherUserId) {
if (userId == null || otherUserId == null) {
return new HashSet<>();
}
Jedis jedis = RedisUtil.getJedis();
String commonKey = COMMON_KEY + userId + "_" + otherUserId;
// 取交集并存于键为commonKey的set中
jedis.zinterstore(commonKey, FOLLOWING + userId, FOLLOWING + otherUserId);
Set<String> result = jedis.zrange(commonKey, 0, -1);
jedis.del(commonKey);
return result;
}
/**
* 根据key获取有序集set
* @param key key
* @return set
*/
private static Set<String> findSet(String key) {
if (key == null) {
return new HashSet<>();
}
Jedis jedis = RedisUtil.getJedis();
// 按照score从大到小排序
return jedis.zrevrange(key, 0, -1);
}
}
- 好友推荐的实现
采用关联规则挖掘算法实现好友推荐,典型的推荐算法如:Apriori、FP-Growth及相关改进算法。
3 参考
https://blog.csdn.net/chengqiuming/article/details/79189955
https://blog.csdn.net/u013239111/article/details/81201404