本文可作为redis in action第七章的读书笔记
需求背景
职位数据库里有这样的数据job1:skill1,skill2,skill4
job2:skill3,skill2
而每一个应聘者,都有若干个技能
给定一个应聘者,从库里筛选出他能胜任(职位要求的仅能这个人必须都具备,当然他有更多的职位并没有要求的技能也是OK的)的职位
方案一
这也是最符合我们第一思维的逻辑有一个set域 job:jobid
里面存放了这个职位需要的技能
在检查某个人是否能胜任某个职位的时候
用job:jobid与它本人的技能求差集
如果差集的size等于0说明职位要求的技能,这个人都具备,他可以胜任这个工作
如果差集的size大于0说明职位要求的技能,这个人并不都具备,他不能胜任这个工作
public void addJob(Jedis conn, String jobId, String... requiredSkills) {
conn.sadd("job:" + jobId, requiredSkills);
}
@SuppressWarnings("unchecked")
public boolean isQualified(Jedis conn, String jobId, String... candidateSkills) {
String temp = UUID.randomUUID().toString();
Transaction trans = conn.multi();
for(String skill : candidateSkills) {
trans.sadd(temp, skill);
}
trans.expire(temp, 5);
trans.sdiff("job:" + jobId, temp);
List<Object> response = trans.exec();
Set<String> diff = (Set<String>)response.get(response.size() - 1);
return diff.size() == 0;
}
测试代码:
public class Chapter07Test {
static Jedis conn = null;
static Chapter07 c=null;
@BeforeClass
public static void initConn(){
System.out.println("test before");
conn = new Jedis("10.150.0.80");
conn.auth("dlf123123");
c=new Chapter07();
}
@Test
public void testIsQualifiedForJob() {
System.out.println("\n----- testIsQualifiedForJob -----");
c.addJob(conn, "test", "q1", "q2", "q3");
//返回true
System.out.println(c.isQualified(conn, "test", "q1", "q3", "q2","q4"));
//返回false
System.out.println(c.isQualified(conn, "test", "q1", "q2"));
}
}
OK,上面的代码能正常运行 那么有问题么?
如果库里有10000个职位,那我就得判断10000次
这就是问题
方案二
还有第二种方式假如有10000个职位,每个职位都有若干个所需技能
我先算出某个人对于这10000个职位里,每个职位都具备几个技能--这个我们称为set-a
然后我还得计算出,每个职位都需要几个技能--这个我们称为set-b
我用setb与seta相减,结果仍然是一个zset集合,member是职位,score等于0的话就说明这个人是能胜任这个工作的#
我们先设计下面几个域
idx:skill:sillId--set集合
这里面列出了,需要这个技能的所有职位信息
idx:jobs:req---zset集合
member是职位 score是这个职位需要的技能的数量
首先将职位与技能添加到库里
public void indexJob(Jedis conn, String jobId, String... skills) {
Transaction trans = conn.multi();
Set<String> unique = new HashSet<String>();
for (String skill : skills) {
trans.sadd("idx:skill:" + skill, jobId);
unique.add(skill);
}
// 记录每个职位需要的技能数量
trans.zadd("idx:jobs:req", unique.size(), jobId);
trans.exec();
}
好了,我们现在先不看代码了,先说说我们得了解的redis的几个命令
Zunionstore 命令
redis 127.0.0.1:6379> ZRANGE programmer 0 -1 WITHSCORES
1) "peter"
2) "2000"
3) "jack"
4) "3500"
5) "tom"
6) "5000"
redis 127.0.0.1:6379> ZRANGE manager 0 -1 WITHSCORES
1) "herry"
2) "2000"
3) "mary"
4) "3500"
5) "bob"
6) "4000"
# 一共有2个待处理集合分别是programmer和manager
# 给他们的工资分别乘以1和3 我艹 为啥?
# 最后那两个新的工资单 相加
# 结果放到salary集合中
# 公司决定加薪。。。除了程序员。。。
redis 127.0.0.1:6379> ZUNIONSTORE salary 2 programmer manager WEIGHTS 1 3
(integer) 6
redis 127.0.0.1:6379> ZRANGE salary 0 -1 WITHSCORES
1) "peter"
2) "2000"
3) "jack"
4) "3500"
5) "tom"
6) "5000"
7) "herry"
8) "6000"
9) "mary"
10) "10500"
11) "bob"
12) "12000"
另外对于Zunionstore命令来说,如果待操作的集合并不是有序集合(也就是说没有score域),那么系统就默认认为它的score是1
更详细的见: http://www.runoob.com/redis/sorted-sets-zunionstore.html
那么我们怎么求出某个用户在每个职位下能满足几个技能要求呢?
假如我有c++与db这两个技能
那我把idx:skill:c++和idx:skill:db这两个域都看做zset(score都是1)
然后相加 如果后台工程师这个职位本身既在c++这个技能要求里,也在db这个技能要求里
最后得出的数据域里后台工程师这个member对应的score就是2
那么下一个问题
我用setb与seta相减,结果仍然是一个zset集合,member是职位,score等于0的话就说明这个人是能胜任这个工作的#
怎么办?WEIGHTS设为-1不就OK了?
上面那个命令是求并集呀?
去看这个命令:ZINTERSTORE
http://doc.redisfans.com/sorted_set/zinterstore.html
OK,再加上下面的代码就搞定了
这么简单?
主要是redis提供了很多强大的命令
public Set<String> findJobs(Jedis conn, String... candidateSkills) {
String[] keys = new String[candidateSkills.length];
double[] weights = new double[candidateSkills.length];
for (int i = 0; i < candidateSkills.length; i++) {
keys[i] = "skill:" + candidateSkills[i];
weights[i] = 1.0;
}
Transaction trans = conn.multi();
//这里的idx:jobScores
//member是职位 score是我能满足这个职位的技能的数量
//这里面是所有跟我还有点"关系"的职位
//国务院总理这个职位所要求的技能 我一个都没有
//那么 这个idx:jobScores里肯定就没有国务院总理这个职位
String jobScores = zunion(
trans, 100, new ZParams().weightsByDouble(weights), keys);
System.out.println("jobscores: zrange idx:"+jobScores+" 0 -1 withscores");
// 用需要的技能数 减去我现在有的技能数
String finalResult = zintersect(
trans, 100, new ZParams().weightsByDouble(-1, 1), jobScores, "jobs:req");
System.out.println("jobscores: zrange idx:"+finalResult+" 0 -1 withscores");
trans.exec();
return conn.zrangeByScore("idx:" + finalResult, 0, 0);
}
public String zintersect(Transaction trans, int ttl,
ZParams params, String... sets){
return zsetCommon(trans, "zinterstore", ttl, params, sets);
}
public String zunion(Transaction trans, int ttl,
ZParams params, String... sets) {
return zsetCommon(trans, "zunionstore", ttl, params, sets);
}
//把zunionstore和zinterstore提取出来
//使用反射技术 不懂的自己去看反射
//这里产生的中间数据 都不必一直保存 所有有个ttl
private String zsetCommon(Transaction trans, String method, int ttl,
ZParams params, String... sets) {
String[] keys = new String[sets.length];
for (int i = 0; i < sets.length; i++) {
keys[i] = "idx:" + sets[i];
}
String id = UUID.randomUUID().toString();
try{
Method m=trans.getClass().getDeclaredMethod(method, String.class,
ZParams.class, String[].class);
System.out.println("method: "+method);
m.invoke(trans,"idx:" + id, params, keys);
}catch(Exception e){
throw new RuntimeException(e);
}
trans.expire("idx:" + id, ttl);
return id;
}
再看看测试代码:
@Test
public void testIndexAndFindJobs() {
System.out.println("\n----- testIndexAndFindJobs -----");
c.indexJob(conn, "test1", "q1", "q2", "q3");
c.indexJob(conn, "test2", "q1", "q3", "q4");
c.indexJob(conn, "test3", "q1", "q3", "q5");
// System.out.println(c.findJobs(conn, "q1").size());
Iterator<String> result =null;
// result=c.findJobs(conn, "q1", "q3", "q4").iterator();
// printIterator(result);
result = c.findJobs(conn, "q1", "q3", "q5").iterator();
printIterator(result);
result = c.findJobs(conn, "q8", "q9", "q310", "q11", "q12").iterator();
printIterator(result);
}
测试结果:
test before
----- testIndexAndFindJobs -----
method: zunionstore
jobscores: zrange idx:51926856-67fd-42ba-8620-fa90cae4f4d8 0 -1 withscores
method: zinterstore
jobscores: zrange idx:b6df8812-f725-46fd-a121-a08c75248d5a 0 -1 withscores
test3
abc
method: zunionstore
jobscores: zrange idx:784ce6b6-5218-4e4d-ac53-b235cf317374 0 -1 withscores
method: zinterstore
jobscores: zrange idx:990004c6-2c9e-4c26-95b9-248dbf2a5864 0 -1 withscores
abc