Redis-Set专题

redis 的set类型 和 Java的set集合功能类似,

set的主要功能是求 并集,交集,差集

1.淘宝黑名单校验器的redis技术方案

1、黑名单过滤器业务场景分析


淘宝的商品评价功能,不是任何人就能评价的,有一种职业就是差评师,差评师就是勒索敲诈商家,
这种差评师在淘宝里面就被设置了黑名单,即使购买了商品,也评价不了。


2、黑名单校验器的redis技术方案

黑名单过滤器除了针对上文说的淘宝评价,针对用户黑名单外,其实还有ip黑名单、设备黑名单等。  
在高并发的情况下,通过数据库过滤明显不符合要求,一般的做法都是通过Redis来实现的。  
那redis那种数据结构适合做这种黑名单的呢?   
答案是:set
步骤1:先把数据库的数据同步到redis的set集合中。  
步骤2:评价的时候验证是否为黑名单,通过sismember命令来实现。

3.实现:

1.提前先把数据刷新到redis缓存中


/**
 * 提前先把数据刷新到redis缓存中
 */
@PostConstruct
public void init(){
    log.info("启动初始化 ..........");
    List blacklist=this.blacklist();
    //this.redisTemplate.delete(Constants.BLACKLIST_KEY);
    blacklist.forEach(t->this.redisTemplate.opsForSet().add(Constants.BLACKLIST_KEY,t));
}

/**
 * 模拟100个黑名单
 */
public List blacklist() {
    List list=new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        list.add(i);
    }
    return list;
}
```

2.编写黑名单校验器接口

/**
 *编写黑名单校验器接口
 * true=黑名单
 * false=不是黑名单
 */
@GetMapping(value = "/isBlacklist")
public boolean isBlacklist(Integer userId) {
    boolean bo=false;
    try {
        //到set集合中去校验是否黑名单,
        bo = this.redisTemplate.opsForSet().isMember(Constants.BLACKLIST_KEY,userId);
        log.info("查询结果:{}", bo);
    } catch (Exception ex) {
        //这里的异常,一般是redis瘫痪 ,或 redis网络timeout
        log.error("exception:", ex);
        //TODO 走DB查询
    }
    return bo;
}
```

2.京东京豆抽奖的技术方案

京豆抽奖一般是采用redis的set集合来操作的,那为什么是set集合适用于抽奖呢?
2个原因:
1.set集合的特点是元素不重复 存放1个 5个 10个京豆 谢谢参与
2.set集合支持随机读取
具体的技术方案是采用set集合的srandmember命令来实现,随机返回set的一个元素

案例实战:

步骤1:奖品的初始化
由于set集合是不重复,故在奖品初始化的时候,要为每个奖品设置一个序列号

``` 
    /**
     *提前先把数据刷新到redis缓存中。
     */
    @PostConstruct
    public void init(){
        log.info("启动初始化..........");

        boolean bo=this.redisTemplate.hasKey(Constants.PRIZE_KEY);
        if(!bo){
            List crowds=this.prize();
            crowds.forEach(t->this.redisTemplate.opsForSet().add(Constants.PRIZE_KEY,t));
        }
    }

    /**
     *按一定的概率初始化奖品
     */
    public List prize() {
        List list=new ArrayList<>();
        //10个京豆,概率10%
        for (int i = 0; i < 10; i++) {
            list.add("10-"+i);
        }
        //5个京豆,概率20%
        for (int i = 0; i < 20; i++) {
            list.add("5-"+i);
        }
        //1个京豆,概率60%
        for (int i = 0; i < 60; i++) {
            list.add("1-"+i);
        }
        //0个京豆,概率10%
        for (int i = 0; i < 10; i++) {
            list.add("0-"+i);
        }
        return list;
    }
```

步骤2:抽奖



    @GetMapping(value = "/prize")
    public String prize() {
        String result="";
        try {
            //随机取1次。
            String object = (String)this.redisTemplate.opsForSet().randomMember(Constants.PRIZE_KEY);
            if (!StringUtils.isEmpty(object)){
                //截取序列号 例如10-1
                int temp=object.indexOf('-');
                int no=Integer.valueOf(object.substring(0,temp));
                switch (no){
                    case 0:
                        result="谢谢参与";
                        break;
                    case 1:
                        result="获得1个京豆";
                        break;
                    case 5:
                        result="获得5个京豆";
                        break;
                    case 10:
                        result="获得10个京豆";
                        break;
                    default:
                        result="谢谢参与";
                }
            }
            log.info("查询结果:{}", object);
        } catch (Exception ex) {
            log.error("exception:", ex);
        }
        return result;
    }
```

3.支付宝抽奖的技术方案

思考一个问题:支付宝的抽奖 和 京东京豆的抽奖有什么区别????
1. 京豆抽奖:奖品是可以重复,例如抽5京豆可以再抽到5京豆,即京豆是无限量抽。
2. 支付宝抽奖: 奖品不能重复抽,例如1万人抽1台华为手机;再给大家举一个熟悉的例子:
 例如公司年会,抽中奖品的人,下一轮就不能重复抽取,不然就会重复中奖。

技术方案和京东的京豆类似,但是不同的是
京东的京豆用了srandmember命令,即随机返回set的一个元素
支付宝的抽奖要用spop命令,即随机返回并删除set中一个元素
为什么呢?
因为支付宝的奖品有限,不能重复抽,故抽奖完后,必须从集合中剔除中奖的人。
再举个每个人都参与过的例子,年会抽奖,你公司1000人,年会抽奖3等奖500名100元,2等奖50名1000元,1等奖10名10000元,
在抽奖的设计中就必须把已中奖的人剔除,不然就会出现重复中奖的概率。

案例实战:SpringBoot+Redis 实现支付宝抽奖

这个方法的逻辑是:使用set存储用户id,当抽某个奖品时,将奖品数量传入setOperations.pop

中,然后set会随机抽出对应数量的用户,并将其删除

#####步骤1:初始化抽奖数据
``` 
    /**
     *提前先把数据刷新到redis缓存中。
     */
    @PostConstruct
    public void init(){
        log.info("启动初始化..........");

        boolean bo=this.redisTemplate.hasKey(Constants.PRIZE_KEY);
        if(!bo){
            List crowds=this.prize();
            crowds.forEach(t->this.redisTemplate.opsForSet().add(Constants.PRIZE_KEY,t));
        }
    }

    /**
     * 模拟10个用户来抽奖 list存放的是用户id
     * 例如支付宝参与抽奖,就把用户id加入set集合中
     * 例如公司抽奖,把公司所有的员工,工号都加入到set集合中
     */
    public List prize() {
        List list=new ArrayList<>();
        for(int i=1;i<=10;i++){
            list.add(i);
        }
        return list;
    }
```
#####步骤2:抽奖逻辑
``` 
    @GetMapping(value = "/prize")
    public List prize(int num) {
        try {
            SetOperations setOperations= this.redisTemplate.opsForSet();
            //spop命令,即随机返回并删除set中一个元素
            List objs = setOperations.pop(Constants.PRIZE_KEY,num);
            log.info("查询结果:{}", objs);
            return  objs;
        } catch (Exception ex) {
            log.error("exception:", ex);
        }
        return null;
    }
```

当然上述逻辑不适合大规模用户抽奖,也不适用于抽奖活动天数为几天甚至更长的场景。

抽奖活动天数较长,参考:使用redis zset实现抽奖,奖池商品按时间随机分布 - T~Z - 博客园

4. SpringBoot+Redis 实现好友\QQ群 随机推荐


### 随机展示业务场景分析
思考题:为什么要随机展示?
因为展示的区域有限啊,在那么小的地方展示全部数据是不可能的,通常的做法就是随机展示一批数据,然后用户点击“换一换”按钮,再随机展示另一批。


### 随机展示的redis技术方案
上文已经说了随机展示的原因就是区域有限,而区域有限的地方通常就是首页或频道页,这些位置通常都是访问量并发量非常高的,
一般是不可能采用数据库来实现的,通常都是Redis来实现。
redis的实现技术方案:
步骤1:先把数据准备好,把所有需要展示的内容存入redis的Set数据结构中。
步骤2:通过srandmember命令随机拿一批数据出来。


### SpringBoot+Redis 实现高并发随机展示

#### SpringBoot+Redis 实现好友\QQ群 随机推荐
##### 步骤1:提前先把数据刷新到redis缓存中。
``` 
    /**
     *提前先把数据刷新到redis缓存中。
     */
    @PostConstruct
    public void init(){
        log.info("启动初始化 群..........");
        List crowds=this.crowd();
        this.redisTemplate.delete(Constants.CROWD_KEY);
        crowds.forEach(t->this.redisTemplate.opsForSet().add(Constants.CROWD_KEY,t));
    }

    /**
     * 模拟100个热门群,用于推荐
     */
    public List crowd() {
        List list=new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Random rand = new Random();
            int id= rand.nextInt(10000);
            list.add("群"+id);
        }
        return list;
    }
```
##### 步骤2:编写随机查询接口
``` 
@GetMapping(value = "/crowd")
public List crowd() {
    List list=null;
    try {
        //采用redis set数据结构,随机取出10条数据
        list = this.redisTemplate.opsForSet().randomMembers(Constants.CROWD_KEY,10);
        log.info("查询结果:{}", list);
    } catch (Exception ex) {
        //这里的异常,一般是redis瘫痪 ,或 redis网络timeout
        log.error("exception:", ex);
        //TODO 走DB查询
    }
    return list;
}
```

5.SpringBoot+Redis 微博榜单随机推荐


微博榜单和QQ群的区别是:微博榜单是整块数据的,所以随机的数据要按块来推荐
所以我们要定义一个java bean来包装整块数据
 

##### 步骤1:提前先把数据刷新到redis缓存中。

数据类:

``` 
@Data
public class WeiboList {
    
    private int id;
    /**
     * 榜单名称
     */
    private String name;

    private List users;

}
```
``` 


    /**
     * 模拟10个热门榜单,用于推荐
     */
    public List list() {
        List list=new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            WeiboList wl=new WeiboList();
            wl.setId(i);
            wl.setName("榜单"+i);
            Random rand = new Random();
            List users=new ArrayList<>();
            for (int j=0;j<3;j++){
                int id= rand.nextInt(10000);
                users.add("user:"+id);
            }
            wl.setUsers(users);
            list.add(wl);
        }
        return list;
    }
```

6.微博点赞实战

#### 一、微博点赞业务场景分析
梳理点赞的业务场景,它有2个接口:
第一个:点赞或取消点赞,用户点击功能
第二个接口:查看帖子信息:通过用户id 和帖子id,查看该帖子的点赞数、该用户是否点赞状态。


#### 二、微博点赞的技术方案
点赞的关键技术就是要判断该用户是否点赞,已重复点赞的不允许再点赞,即过滤重复,虽然业务不复杂,可以采用数据库直接实现,
但是对于微博这种高并发的场景,不可能查数据库的,一般是缓存,即redis
ok,我们来对上文梳理的2个接口进行技术分析:
第一个:点赞或取消点赞,用户点击功能

采用的是redis的set数据结构,key=like:postid    value={userid}

postid :帖子id

采用sadd命令,添加点赞
```
127.0.0.1:6379> sadd like:1000 101
(integer) 1
127.0.0.1:6379> sadd like:1000 102
(integer) 1
127.0.0.1:6379> sadd like:1000 103
(integer) 1
127.0.0.1:6379> smembers like:1000
1) "101"
2) "102"
3) "103"
```
采用srem命令,取消点赞
```
127.0.0.1:6379> srem like:1000 101
(integer) 1
127.0.0.1:6379> smembers like:1000
1) "102"
2) "103"
```

第二个接口:查看帖子信息:通过用户id 和帖子id,查看该帖子的点赞数、该用户是否点赞状态。
采用scard命令,点赞总数
```
127.0.0.1:6379> smembers like:1000
1) "102"
2) "103"
127.0.0.1:6379> scard like:1000
(integer) 2
```
采用sismember命令,判断是否点赞
```
127.0.0.1:6379> smembers like:1000
1) "102"
2) "103"
127.0.0.1:6379> sismember like:1000 102
(integer) 1
127.0.0.1:6379> sismember like:1000 101
(integer) 0
```

三、案例实战:SpringBoot+Redis 实现微博点赞




#####步骤1:点赞逻辑
``` 

    /**
     * 点赞
     */
    @GetMapping(value = "/dolike")
    public String dolike(int postid,int userid) {
        String result="";
        try {
            String key=Constants.LIKE_KEY+postid;
            long object = this.redisTemplate.opsForSet().add(key,userid);
            if (object==1){
                result="点赞成功";
            }else{
                result="你已重复点赞";
            }
            log.info("查询结果:{}", object);
        } catch (Exception ex) {
            log.error("exception:", ex);
        }
        return result;
    }

    /**
     * 取消点赞
     */
    @GetMapping(value = "/undolike")
    public String undolike(int postid,int userid) {
        String result="";
        try {
            String key=Constants.LIKE_KEY+postid;
            long object = this.redisTemplate.opsForSet().remove(key,userid);
            if (object==1){
                result="取消成功";
            }else{
                result="你已重复取消点赞";
            }
            log.info("查询结果:{}", object);
        } catch (Exception ex) {
            log.error("exception:", ex);
        }
        return result;
    }

    /**
     * 根据postid userid查看帖子信息,返回结果是点赞总数和是否点赞
     */
    @GetMapping(value = "/getpost")
    public Map getpost(int postid,int userid) {
        Map map=new HashMap();

        String result="";
        try {
            String key=Constants.LIKE_KEY+postid;
            long size = this.redisTemplate.opsForSet().size(key);
            boolean bo=this.redisTemplate.opsForSet().isMember(key,userid);
            map.put("size",size);
            map.put("isLike",bo);
            log.info("查询结果:{}", map);
        } catch (Exception ex) {
            log.error("exception:", ex);
        }
        return map;
    }

    /**
     * 查看点赞明细,就是有哪些人
     */
    @GetMapping(value = "/likedetail")
    public Set likedetail(int postid) {
        Set set=null;
        try {
            String key=Constants.LIKE_KEY+postid;
            set = this.redisTemplate.opsForSet().members(key);
            log.info("查询结果:{}", set);
        } catch (Exception ex) {
            log.error("exception:", ex);
        }
        return set;
    }
```

7. 微博实战:微博关注与粉丝

# 基于Redis的微博关注与粉丝

### 一、微博关注与粉丝的业务场景分析
阿甘关注了雷军:阿甘就是雷军的粉丝follower
雷军被阿甘关注:雷军就是阿甘的关注followee

### 二、微博关注与粉丝的redis技术方案
技术方案:每个用户都有2个集合:关注集合和粉丝集合
例如 阿甘关注了雷军,做了2个动作

1.把阿甘的userid加入雷军的粉丝follower集合set
2.把雷军的userid加入阿甘的关注followee集合set
 集合key设计
 阿甘的关注集合 key=followee:阿甘userid
 雷军的粉丝集合 key=follower:雷军userid

### SpringBoot+Redis 实现微博关注与粉丝

    /**
     * 阿甘关注了雷军
     */
    public void follow(Integer userId,Integer followeeId){
        SetOperations opsForSet = redisTemplate.opsForSet();
        //阿甘的关注集合
        String followeekey = Constants.CACHE_KEY_FOLLOWEE + userId;
        //把雷军的followeeId,加入阿甘的关注集合中
        opsForSet.add(followeekey,followeeId);

        //雷军的粉丝集合
        String followerkey=Constants.CACHE_KEY_FOLLOWER+followeeId;
        //把阿甘的userid加入雷军的粉丝follower集合set
        opsForSet.add(followerkey,userId);

    }

    /**
     * 查看我的粉丝
     */
    public List myFollower(Integer userId){
        SetOperations opsForSet = redisTemplate.opsForSet();
        //粉丝集合
        String followerkey=Constants.CACHE_KEY_FOLLOWER+userId;
        Set sets= opsForSet.members(followerkey);

        return this.getUserInfo(sets);
    }

    /**
     * 查看我的关注
     */
    public List myFollowee(Integer userId){
        SetOperations opsForSet = redisTemplate.opsForSet();
        //关注集合
        String followeekey = Constants.CACHE_KEY_FOLLOWEE + userId;
        Set sets= opsForSet.members(followeekey);
        return this.getUserInfo(sets);
    }

    /**
     * 求2个用户的关注交集
     */
    public List intersect(Integer userId1,Integer userId2){
        SetOperations opsForSet = redisTemplate.opsForSet();

        String followeekey1 = Constants.CACHE_KEY_FOLLOWEE + userId1;
        String followeekey2 = Constants.CACHE_KEY_FOLLOWEE + userId2;
        //求2个集合的交集
        Set sets= opsForSet.intersect(followeekey1,followeekey2);
        return this.getUserInfo(sets);
    }


    /**
     * 获取用户信息
     */
    private List getUserInfo(Set set){
        List list=new ArrayList<>();

        List hashKeys=new ArrayList<>();
        hashKeys.add("id");
        hashKeys.add("username");

        HashOperations opsForHash=redisTemplate.opsForHash();
        for (Integer id:set) {
            String hkey = Constants.CACHE_KEY_USER + id;
            List clist = opsForHash.multiGet(hkey, hashKeys);
            //redis没有去db找
            if (clist.get(0)==null && clist.get(1)==null){
                //到数据库查
                User user=this.getUserDB(id);

                UserVO vo=new UserVO();
                vo.setId(user.getId());
                vo.setUsername(user.getUsername());
                list.add(vo);
            }else{
                UserVO vo=new UserVO();
                vo.setId(clist.get(0)==null?0:Integer.valueOf(clist.get(0).toString()));
                vo.setUsername(clist.get(1)==null?"":clist.get(1).toString());
                list.add(vo);
            }
        }
        return list;
    }

    public User getUserDB(Integer userid) {
        User obj = this.userMapper.selectByPrimaryKey(userid);
        //将Object对象里面的属性和值转化成Map对象
        Map map = ObjectUtil.objectToMap(obj);
        //设置缓存key
        String key = Constants.CACHE_KEY_USER + obj.getId();

        //微博用户的存储采用reids的hash
        HashOperations opsForHash = redisTemplate.opsForHash();
        opsForHash.putAll(key, map);

        //设置过期30天
        this.redisTemplate.expire(key, 30, TimeUnit.DAYS);
        return obj;
    }
}

8.微博实战:微关系计算

Redis-Set专题_第1张图片

### 一、计算好友关系业务场景分析

 共同关注:是计算出阿甘和雷军共同关注的人有哪些?
 我关注的人也关注他:是计算出我阿甘关注的人群中,有哪些人同时和我一样关注了雷军

### 二、计算好友关系的redis技术方案
思考题:如果是采用数据库来实现用户的关系,一般SQL怎么写? 例如 阿甘关注10个人,雷军关注100个人,让你计算2人的共同关注那些人?

SQL的写法,一般是采用in 或 not in 来实现。但是对于互联网高并发的系统来说,in  not in 明显不适合。  
一般的做法是采用redis的set集合来实现。  
Redis Set数据结构,非常适合存储好友、关注、粉丝、感兴趣的人的集合。然后采用set的命令就能得出我们想要的数据。  
1. sinter命令:获得A和B两个用户的共同好友  
2. sismember命令:判断C是否为B的好友  
3. scard命令:获取好友数量  



### SpringBoot+Redis 计算微博好友关系
目标:共同关注:是计算出阿甘和雷军共同关注的人有哪些?
``` 
@ApiOperation(value="求2个用户的关注交集")
@GetMapping(value = "/intersect")
public List intersect(Integer userId1, Integer userId2){
    return  this.relationService.intersect(userId1,userId2);
}
```
``` 
/**
 * 求2个用户的关注交集
 */
public List intersect(Integer userId1,Integer userId2){
    SetOperations opsForSet = redisTemplate.opsForSet();

    String followeekey1 = Constants.CACHE_KEY_FOLLOWEE + userId1;
    String followeekey2 = Constants.CACHE_KEY_FOLLOWEE + userId2;
    //求2个集合的交集
    Set sets= opsForSet.intersect(followeekey1,followeekey2);
    return this.getUserInfo(sets);
}

9.淘宝亿级的UV计算

UV(Unique Visitor)独立访客,统计1天内访问某站点的用户数(以cookie为依据);访问网站的一台电脑客户端为一个访客。可以理解成访问某网站的电脑的数量。网站判断来访电脑的身份是通过来访电脑的cookies实现的。如果更换了IP后但不清除cookies,再访问相同网站,该网站的统计中UV数是不变的。如果用户不保存cookies访问、清除了cookies或者更换设备访问,计数会加1。00:00-24:00内相同的客户端多次访问只计为1个访客。

UV、PV是对网站流量的统计,适用于计算机网站和触屏版网站。

APP对用户的判断标准不再是UV、PV,而是新增用户、活跃用户、启动次数。

Redis-Set专题_第2张图片

Redis-Set专题_第3张图片 参考:11. 优秀的基数统计算法--HyperLogLog - 古明地盆 - 博客园

 Redis-Set专题_第4张图片

###  HyperLogLog 命令详解
HyperLogLog 目前只支持3个命令,PFADD、PFCOUNT、PFMERGE
#### PFADD
将元素加入到HyperLogLog数据结构中,如果 HyperLogLog 的基数估算值在命令执行之后出现了变化,那么命令返回1,否则返回0。

#### PFCOUNT
返回给定 HyperLogLog 的基数估算值。
```
127.0.0.1:6379> pfadd uv 192.168.1.100 192.168.1.101 192.168.1.102 192.168.1.103
(integer) 1
127.0.0.1:6379> pfadd uv 192.168.1.100
(integer) 0
127.0.0.1:6379> pfcount uv
(integer) 4
```

#### PFMERGE
把多个HyperLogLog合并为一个HyperLogLog,合并后的HyperLogLog的基数是通过所有的HyperLogLog进行并集后,得出来的。
```
127.0.0.1:6379> pfadd uv1 192.168.1.100 192.168.1.101 192.168.1.102 192.168.1.103
(integer) 1
127.0.0.1:6379> pfcount uv1
(integer) 4
127.0.0.1:6379> pfadd uv2 192.168.1.100 192.168.2.101 192.168.2.102 192.168.2.103
(integer) 1
127.0.0.1:6379> pfadd uv3 192.168.1.100 192.168.3.101 192.168.3.102 192.168.3.103
(integer) 1
127.0.0.1:6379> pfmerge aganuv uv1 uv2 uv3
OK
127.0.0.1:6379> pfcount aganuv
(integer) 10
```
这个命令很重要,例如你可以统计一周 或 一个月的uv 就可以使用此命令来轻易实现。

### 3.案例实战:基于Redis的UV计算

#### 步骤1:模拟UV访问

public class TaskService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     *模拟UV访问
     */
    @PostConstruct
    public void init(){
        log.info("模拟UV访问 ..........");
        new Thread(()->this.refreshData()).start();
    }

    /**
     * 刷新当天的统计数据
     */
    public void refreshDay(){
        Random rand = new Random();
        String ip=rand.nextInt(999)+"."+
                rand.nextInt(999)+"."+
                rand.nextInt(999)+"."+
                rand.nextInt(999);

        //redis 命令 pfadd
        long n=this.redisTemplate.opsForHyperLogLog().add(Constants.PV_KEY,ip);
        log.debug("ip={} , returen={}",ip,n);
    }

    /**
     * 按天模拟UV统计
     */
    public void refreshData(){
        while (true){
            this.refreshDay();
            //TODO 在分布式系统中,建议用xxljob来实现定时
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
```
#### 步骤2:实现UV统计功能
``` 
@RestController
@Slf4j
public class Controller {

    @Autowired
    private RedisTemplate redisTemplate;
    

    @GetMapping(value = "/uv")
    public long uv() {
        //redis命令 pfcount
        long size=this.redisTemplate.opsForHyperLogLog().size(Constants.PV_KEY);
        return size;
    }

}
```

#### 步骤2:实现UV统计功能


@RestController
@Slf4j
public class Controller {

    @Autowired
    private RedisTemplate redisTemplate;
    

    @GetMapping(value = "/uv")
    public long uv() {
        //redis命令 pfcount
        long size=this.redisTemplate.opsForHyperLogLog().size(Constants.PV_KEY);
        return size;
    }

}
```





你可能感兴趣的:(redis,redis,缓存,数据库)