Redis数据结构Set应用场景--黑名单校验器、京东与支付宝抽奖、微博榜单与QQ群的随机展示、帖子点赞、关注与粉丝、微关系计算、HyperLogLog的入门使用

set命令使用

redis set 和java的set集合功能差不多的

  • 集合(Set) 的主要功能就是求并集、交集、差集。
A = {'a', 'b', 'c'}
B = {'a', 'e', 'i', 'o', 'u'}

inter(x, y): 交集,在集合x和集合y中都存在的元素。
inter(A, B) = {'a'}

union(x, y): 并集,在集合x中或集合y中的元素,如果一个元素在x和y中都出现,那只记录一次即可。
union(A,B) = {'a', 'b', 'c', 'e', 'i', 'o', 'u'}

diff(x, y): 差集,在集合x中而不在集合y中的元素。
diff(A,B) = {'b', 'c'}

card(x): 基数,一个集合中元素的数量。
card(A) = 3

空集: 基数为0的集合。
  • sadd(key, member)
    向名称为key的set中添加元素member
  • smembers(key)
    返回名称为key的set的所有元素
127.0.0.1:6379> sadd users u1
(integer) 1
127.0.0.1:6379> sadd users u2
(integer) 1
127.0.0.1:6379> sadd users u3 u4
(integer) 2
127.0.0.1:6379> smembers users
1) "u2"
2) "u4"
3) "u1"
4) "u3"
  • srem(key, member) :
    删除名称为key的set中的元素member
127.0.0.1:6379> smembers users
1) "u2"
2) "u4"
3) "u1"
4) "u3"
127.0.0.1:6379> srem users u1
(integer) 1
127.0.0.1:6379> srem users u3 u4
(integer) 2
127.0.0.1:6379> smembers users
1) "u2"
  • sismember(key, member) :
    member是否是名称为key的set的元素
127.0.0.1:6379> smembers users
1) "u2"
127.0.0.1:6379> sismember users u2
(integer) 1
127.0.0.1:6379> sismember users u1
(integer) 0
  • scard(key) :
    返回名称为key的set的基数,一个集合中元素的数量。
127.0.0.1:6379> smembers users
1) "u2"
127.0.0.1:6379> scard users
(integer) 1
  • smove(srckey, dstkey, member) :
    将member元素从source集合移动到destination集合。
127.0.0.1:6379> smembers users
1) "u2"
2) "u4"
3) "u1"
4) "u3"
127.0.0.1:6379> smembers blacklist
(empty list or set)
127.0.0.1:6379> smove users blacklist u1
(integer) 1
127.0.0.1:6379> smembers users
1) "u2"
2) "u4"
3) "u3"
127.0.0.1:6379> smembers blacklist
1) "u1"
  • srandmember(key) :
    随机返回名称为key的set的一个元素
127.0.0.1:6379> smembers users
1) "u2"
2) "u4"
3) "u3"
127.0.0.1:6379> srandmember users
"u3"
127.0.0.1:6379> srandmember users 2
1) "u2"
2) "u3"
  • spop(key) :
    随机返回并删除名称为key的set中一个元素
127.0.0.1:6379> smembers users
1) "u2"
2) "u4"
3) "u3"
127.0.0.1:6379> spop users
"u3"
127.0.0.1:6379> smembers users
1) "u2"
2) "u4"
  • sinter(key1, key2,…key N) :
    求交集。
127.0.0.1:6379> smembers group1
1) "3"
2) "2"
3) "4"
4) "1"
5) "a"
127.0.0.1:6379> smembers group2
1) "b"
2) "a"
3) "1"
4) "c"
127.0.0.1:6379> sinter group1 group2
1) "a"
2) "1"
  • sinterstore(dstkey, (key2,…key N)) :
    求交集并将交集保存到dstkey的集合
127.0.0.1:6379> sinterstore group3 group1 group2
(integer) 2
127.0.0.1:6379> smembers group1
1) "3"
2) "4"
3) "1"
4) "2"
5) "a"
127.0.0.1:6379> smembers group2
1) "b"
2) "a"
3) "1"
4) "c"
127.0.0.1:6379> smembers group3
1) "a"
2) "1"
  • sunion(key1, (keys)) :
    求并集
127.0.0.1:6379> sunion group1 group2
1) "3"
2) "4"
3) "1"
4) "2"
5) "b"
6) "a"
7) "c"
  • sunionstore(dstkey, (keys)) :
    求并集并将并集保存到dstkey的集合
127.0.0.1:6379> sunionstore group4 group1 group2
(integer) 7
127.0.0.1:6379> smembers group4
1) "3"
2) "4"
3) "1"
4) "2"
5) "b"
6) "a"
7) "c"
  • sdiff(key1, (keys)) :
    求差集
127.0.0.1:6379> smembers group1
1) "3"
2) "4"
3) "1"
4) "2"
5) "a"
127.0.0.1:6379> smembers group2
1) "b"
2) "a"
3) "1"
4) "c"
127.0.0.1:6379> sdiff group1 group2
1) "2"
2) "3"
3) "4"

  • diffstore(dstkey, (keys)) :
    求差集并将差集保存到dstkey的集合
127.0.0.1:6379> sdiffstore group5 group1 group2
(integer) 3
127.0.0.1:6379> smembers group5
1) "2"
2) "3"
3) "4"

淘宝黑名单

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

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

二 、解决的技术方案

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

三、SpringBoot+redis模仿实现校验器

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

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

/**
 * 模拟100个黑名单
 */
public List<Integer> blacklist() {
    List<Integer> 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;
}

京东京豆抽奖

一、京东京豆抽奖的业务场景分析

在这里插入图片描述

  1. 可以无线抽,奖品是无限的,不同奖品的概率是不一样的;

二、京东京豆抽奖的技术方案

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

三、SpringBoot+Redis 实现京东京豆抽奖

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

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

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

    /**
     *按一定的概率初始化奖品
     */
    public List<String> prize() {
        List<String> 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;
    }

支付宝抽奖

在这里插入图片描述

二、支付宝抽奖的技术方案

思考一个问题:支付宝的抽奖 和 京东京豆的抽奖有什么区别????

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

SpringBoot+Redis 实现支付宝抽奖

步骤1:初始化抽奖数据

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

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

    /**
     * 模拟10个用户来抽奖 list存放的是用户id
     * 例如支付宝参与抽奖,就把用户id加入set集合中
     * 例如公司抽奖,把公司所有的员工,工号都加入到set集合中
     */
    public List<Integer> prize() {
        List<Integer> list=new ArrayList<>();
        for(int i=1;i<=10;i++){
            list.add(i);
        }
        return list;
    }

步骤2:抽奖逻辑

    @GetMapping(value = "/prize")
    public List<Integer> prize(int num) {
        try {
            SetOperations<String, Integer> setOperations= this.redisTemplate.opsForSet();
            //spop命令,即随机返回并删除set中一个元素
            List<Integer> objs = setOperations.pop(Constants.PRIZE_KEY,num);
            log.info("查询结果:{}", objs);
            return  objs;
        } catch (Exception ex) {
            log.error("exception:", ex);
        }
        return null;
    }

微博榜单与QQ群的随机展示

一、随机展示业务场景分析

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

在这里插入图片描述

二、随机展示的redis技术方案

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

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

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

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

    /**
     * 模拟100个热门群,用于推荐
     */
    public List<String> crowd() {
        List<String> 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<String> crowd() {
    List<String> 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;
}

微博榜单的随机展示

业务场景分析

在这里插入图片描述
他这个换一换,可能包含了不同类型的数据,例如主播榜,美食榜等等、每个榜单里面包含三条数据;

SpringBoot+redis实现微博榜单随机展示

步骤1、创建实体类WeiBoRecommendList

@Data
@ToString
public class WeiBoRecommendList {
    public int id;
    public String name;
    public List<String> user;
}

步骤二、编写初始化数据到Redis中

    public final String COMMEND_KEY="recommend";

    public Object init() {
        ArrayList<WeiBoRecommendList> list = new ArrayList<>();
        for (int i=0; i<20; i++){
            WeiBoRecommendList item= new WeiBoRecommendList();
            item.setId(i);
            item.setName("榜单-"+i);
            Random random=new Random(1);
            ArrayList<String> users = new ArrayList<>();
            for (int index=0; index<3 ; index++){
                int userId = random.nextInt(10000);
                users.add("用户-"+userId);
            }
            item.setUser(users);
            list.add(item);
        }

        list.forEach( t -> redisTemplate.opsForSet().add(COMMEND_KEY,t) );
        return "init ok";
    }

步骤三、编写随机展示接口

@Api("微博推荐榜单")
@RestController
@RequestMapping("/wb")
public class WbRecommendController {
    @Autowired
    WbRecommendService service;

    @PostMapping("/init")
    public Object init(){
       return service.init();
    }
    @PostMapping("/random")
    public Object random(Integer num) {
        Object random = service.random( num);
        return random;
    }
    @PostMapping("/getOne")
    public Object getOne() {
        Object random = service.getOne();
        return random;
    }
}


@Service
public class WbRecommendService {
    @Autowired
    RedisTemplate redisTemplate;

    public final String COMMEND_KEY="recommend";

    public Object init() {
        ArrayList<WeiBoRecommendList> list = new ArrayList<>();
        for (int i=0; i<20; i++){
            WeiBoRecommendList item= new WeiBoRecommendList();
            item.setId(i);
            item.setName("榜单-"+i);
            Random random=new Random(1);
            ArrayList<String> users = new ArrayList<>();
            for (int index=0; index<3 ; index++){
                int userId = random.nextInt(10000);
                users.add("用户-"+userId);
            }
            item.setUser(users);
            list.add(item);
        }

        list.forEach( t -> redisTemplate.opsForSet().add(COMMEND_KEY,t) );
        return "init ok";
    }

    public Object random(Integer num) {
        List list = redisTemplate.opsForSet().randomMembers(COMMEND_KEY, num);
        return list;
    }

    public Object getOne() {
        WeiBoRecommendList member = (WeiBoRecommendList) redisTemplate.opsForSet().randomMember(COMMEND_KEY);
        if (member != null){
            return member;
        }else {
            //TODO -->查询DB
            return null;
        }

    }
}

帖子点赞

一、微博点赞业务场景分析

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

二、微博点赞的技术方案

点赞的关键技术就是要判断该用户是否点赞,已重复点赞的不允许再点赞,即过滤重复,虽然业务不复杂,可以采用数据库直接实现,
但是对于微博这种高并发的场景,不可能查数据库的,一般是缓存,即redis
第一个:点赞或取消点赞,用户点击功能采用的是redis的set数据结构,key=like:postid value={userid}

  • 采用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:点赞逻辑


@Service
public class LikeService {
    @Autowired
    RedisTemplate redisTemplate;

    public final String postKey="post::like::";
    @Transactional
    public Boolean postLike(Integer userId, Integer postId) {
        String key=postKey+postId;
        //TODO 异步插入数据库表;
        Boolean flag = redisTemplate.opsForSet().isMember(key, userId);
        if (!flag){
            redisTemplate.opsForSet().add(key,userId);
            return true;
        }else {
            return false;
        }

    }
    @Transactional
    public Long postUnlike(Integer userId, Integer postId) {
        //TODO 异步删除数据表点赞记录;
        String key=postKey+postId;
        Long flag = redisTemplate.opsForSet().remove(key, userId);
        return flag;
    }
    @Transactional
    public Map getPost(Integer postId, Integer userId) {
        String key=postKey+postId;
        // 获取帖子的内容 ==> Map map=redisTemplate.opsForHash().entries(key)
        //这里偏向Set的使用
        // 获取微博的阅读量==> Long readNum= redisTemplate.opsForValue().get(key)
        //点赞数
        Long likeSize = redisTemplate.opsForSet().size(key);
        //用户是否点赞
        Boolean isLike = redisTemplate.opsForSet().isMember(key, userId);


        HashMap<String, Object> post = new HashMap<>();
        post.put("isLike",isLike);
        post.put("likeSize",likeSize);
        //post.put("title",map.get("title");
        //post.put("content",map.get("content");
        //post.put("readNum",readNum);
        return  post;
    }
}

//接口类

@Api("用户点赞")
@RestController
@RequestMapping("/like")
public class LikeController {

    @Autowired
    LikeService likeService;

    @PostMapping("/postLike")
    public Object postLike(Integer userId , Integer postId){
        Boolean f = likeService.postLike(userId, postId);
        if (f){
            return "post OK";
        }else {
            return "已经点赞了,无法重复点了";
        }
    }

    @PostMapping("/postUnlike")
    public Object postUnlike(Integer userId , Integer postId){
        Long f = likeService.postUnlike(userId, postId);
        if (f > 0){
            return "unlike OK";
        }else {
            return "你还没有点赞或者重复点赞";
        }
    }

    @PostMapping("/getPost")
    public Object getPost(Integer postId,Integer userId){
        Map f = likeService.getPost(postId,userId);
        if (f!=null){
            return f;
        }else {
            //TODO :去DB查询,查询后插入redis;
           // likeService.getPostFromDB(postId,userId);
            return null;
        }
    }
}

关注与粉丝

一、微博关注与粉丝的业务场景分析

我关注了雷军:我就是雷军的粉丝follower;
雷军被阿甘关注:雷军就是阿甘的关注followee;

二、微博关注与粉丝的redis技术方案

技术方案:每个用户都有2个集合:关注集合和粉丝集合
例如 我关注了雷军,做了2个动作

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

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

功能1:关注

    /**
     * 我关注了雷军,我即使雷军的粉丝 follower
     * userId=我
     * followeeId=雷军
     */
    @ApiOperation(value="关注")
    @PostMapping(value = "/follow")
    public void follow(Integer userId,Integer followeeId)   {
        relationService.follow(userId,followeeId);
    }
    
 /**
     * 我关注了雷军
     */
    public void follow(Integer userId,Integer followeeId){
        SetOperations<String, Integer> opsForSet = redisTemplate.opsForSet();
        //我的关注集合
        String followeekey = Constants.CACHE_KEY_FOLLOWEE + userId;
        //把雷军的followeeId,加入我的关注集合中
        opsForSet.add(followeekey,followeeId);
			//TODO:异步插入数据库DB;
        //雷军的粉丝集合
        String followerkey=Constants.CACHE_KEY_FOLLOWER+followeeId;
        //把我的userid加入雷军的粉丝follower集合set
        opsForSet.add(followerkey,userId);
		//TODO:异步插入数据库DB;
    }

功能2:我的关注

    @ApiOperation(value="查看我的关注")
    @GetMapping(value = "/myFollowee")
    public List<UserVO> myFollowee(Integer userId){

        return this.relationService.myFollowee(userId);
    }
    
    /**
     * 查看我的关注
     */
    public List<UserVO> myFollowee(Integer userId){
        SetOperations<String, Integer> opsForSet = redisTemplate.opsForSet();
        //关注集合
        String followeekey = Constants.CACHE_KEY_FOLLOWEE + userId;
        Set<Integer> sets= opsForSet.members(followeekey);
        //查询不到-->就去DB查询;
        return this.getUserInfo(sets);
    }

功能3:我的粉丝

    @ApiOperation(value="查看我的粉丝")
    @GetMapping(value = "/myFollower")
    public List<UserVO> myFollower(Integer userId){
        return this.relationService.myFollower(userId);
    }
    /**
     * 查看我的粉丝
     */
    public List<UserVO> myFollower(Integer userId){
        SetOperations<String, Integer> opsForSet = redisTemplate.opsForSet();
        //粉丝集合
        String followerkey=Constants.CACHE_KEY_FOLLOWER+userId;
        Set<Integer> sets= opsForSet.members(followerkey);
		//sets==null,查询不到就去DB查
        return this.getUserInfo(sets);
    }

功能4:获取用户信息

  private Object getUserInfo(Collection<Integer> members) {
        ArrayList<Object> list = new ArrayList<>();
        for (Integer userId : members){
            Map map = redisTemplate.opsForHash().entries(USER_KEY + userId);
            User user = new User();
            user.setUsername((String) map.get("username"));
            user.setSex((Integer) map.get("sex"));
            user.setId((Integer) map.get("id"));
            list.add(user);
        }
        return list;

    }

微关系计算

在这里插入图片描述

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

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

二、计算好友关系的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<UserVO> intersect(Integer userId1, Integer userId2){
    return  this.relationService.intersect(userId1,userId2);
}
public List<UserVO> intersect(Integer userId1,Integer userId2){
    SetOperations<String, Integer> opsForSet = redisTemplate.opsForSet();

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

我关注的人也关注他

    @ApiOperation("我关注的人也关注他")
    @PostMapping("/getRelation2")
    public Object getRelation2(Integer userId, Integer otherUserId){
        return service.getRelation2(userId,otherUserId);
    }
    
    @Transactional
    public Object getRelation2(Integer userId, Integer otherUserId) {
        String concernKey=CONCERN_KEY+userId;
        Set<Integer> concernList = redisTemplate.opsForSet().members(concernKey);
        List<Integer> resultIds = new ArrayList<>();
        for (Integer concernId : concernList){
            Boolean flag = redisTemplate.opsForSet().isMember(CONCERN_KEY + concernId, otherUserId);
            if (flag){
                resultIds.add(concernId);
            }
        }
        return this.getUserInfo(resultIds);
    }

HyperLogLog

命令使用

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 就可以使用此命令来轻易实现。

基于Redis的UV计算

步骤1:模拟UV访问

@Service
@Slf4j
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;
    }

}
  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值