Redis使用场景(set)

前言

老套路,在开始之前让我们先回顾下set的数据结构—散列表(hashMap)。对于hashMap我们这里不做过多的解释,我们知道hashMap对于某个key第二次put会把第一次的值覆盖掉,成就了set结构 值的唯一性,同时Set结构也保留了无序性。针对这两点我们一起看下redis官方提供的set相关命令。

//基本命令
# 向redis set 中添加元素
sadd myset 1 2 3
# 获取redis set 中的所有元素
smembers myset 
返回  2 1 3 (无序)
#判断是否在集合中
sismember 1   
返回 1 在集合中  0 不在集合中
#删除某个元素
srem 1 
#获取集合的大小
scard myset
#弹出一个元素(返回并删除)
spop myset


//骚操作
#随机获取集合中的元素 (假定元素大小2)
srandmember myset 2  返回两个元素,不重复
srandmember myset 5  返回两个元素,不重复
srandmember myset -2  返回两个元素,可重复

#移动一个元素到另一个集合(一次只能移动一个元素)
smove myset myset 1  (把1移动到myset1集合)

#差集  返回 在第一个set 不在后面任何一个set中的元素
smembers myset 1 2 3   smembers myset1 1  smembers myset2 2
sdiff myset myset1 myset2   (返回 3)

#差集 并保存到一个新的集合 
sdiffstore myset4 myset myset1 myset2

#交集  返回所有集合的交集
sinter myset myset1 ....

#交集并保存
sinterstore myset myset1 ...

#并集  返回多个集合的并集
sunion myset myset1...

#并集并保存
sunionstore myset myset1...

正文

命令决定着使用场景?大家通过前言中的命令可以自己先想一想有哪些场景可以用到我们的set呢?

去重

在这个互联网时代,网络爬虫,恶意攻击已经无处不在,企业中的黑名单往往有着重要的作用去隔离某些恶意攻击的请求。将这些恶意攻击的ip放到redis的set中,是目前很多企业使用的方法之一。不仅如此,白名单也可以维护在redis的set中。

//伪代码
if(redis.sismember(blackSet,iip)){
    //恶意攻击 返回错误
    return "error";
}else if(redis.sismember(whiteSet, ip)){
    //白名单 处理一些白名单逻辑
    doWhiteThing();
}else{
    //处理正常逻辑
    doSomeThing();
}

随机

随机是一个在当下接触到的词频挺高的一个词,那么redis的set随机返回指定数量的元素可以用到的地方也是相当的多了!比如说:随机返回几首歌曲、随机返回几道习题、随机返回一批好友等等。

//推荐音乐
public Set randMusic (){
    return redisTemplate.opsForSet().distinctRandomMembers("music", 10);
}

交并补

redis set的交并补集命令,大家是不是立刻想到了共同好友、感兴趣的人等等。没错这些功能一般都是通过redis的set功能实现的。redis的原生命令支持,让这些看起来不好实现的功能变得如此轻松。

  • 共同好友     —   交集
  • 感兴趣的人    —    并集
  • 好友数      —      集合数量
//返回共同好友
public Set commonFriend (){
    redisTemplate.opsForSet().add("zhangsan", 1 ,2 ,3);
    redisTemplate.opsForSet().add("lisi", 3 ,4 ,5);
    return redisTemplate.opsForSet().intersect("zhangsan","lisi");
}

//感兴趣的人
public Set interestedPeople (){
   redisTemplate.opsForSet().add("zhangsan", 1 ,2 ,3);
   redisTemplate.opsForSet().add("lisi", 3 ,4 ,5);
   Set union = redisTemplate.opsForSet().union("zhangsan", "lisi");
   Set result = Sets.newHashSet();
   union.forEach(e->{if(!redisTemplate.opsForSet().isMember("zhangsan", e)){
       result.add(e);
   }});
   return result;
}

//好友数
public Long count (){
   return redisTemplate.opsForSet().size("zhangsan");
}

扩展

思考!这样做是否有问题?有什么问题?

哈哈,既然都这么问了,那就是有问题!那么究竟是什么问题呢? 首先,该写法在单机、主从、哨兵部署的redis都没有问题,但是在集群情况下可能会出现问题!  没错,大家都知道Redis Cluster一共有16384个slot,每个key都是通过哈希算法获取数值哈希,再模16384来定位slot的。如果同一个命令操作的多个key处在不同的槽(slot)那么redis会报错((error) CROSSSLOT Keys in request don't hash to the same slot )。因为你操作的两个key不在同一个槽,也就是不在同一个分片,当然就会报错。

怎么解决?

其实很简单,通过Hash Tag 将不同的key  hash到同一个槽。当一个key包含 {} 的时候,就不对整个key做hash,而仅对 {} 包括的字符串做hash。 假设hash算法为sha1。对{friend:key}:zhangsan和{friend:key}:lisi,其hash值都等同于sha1(friend:key)。 其实就相当于将共同好友相关的key都强行放到同一个槽中。 ps:实际情况并不会这么暴力,将所有的key都放入到一个槽,而是先通过自定义算法,按照某种规则均匀放入不同的槽。(视具体情况而定)

beta:
  listen: 127.0.0.1:2212
  hash: sha1
  hash_tag: "{}"
  auto_eject_hosts: false
  timeout: 400
  redis: true
  servers:
   - 127.0.0.1:6380:1 server1
   - 127.0.0.1:6381:1 server2
   - 127.0.0.1:6382:1 server3
   - 127.0.0.1:6383:1 server4