打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Redis实现求交集操作结果缓存的设计方案

Redis的集合操作

实话说,Redis提供的集合操作是我选择它成为内存数据库的一个主要理由,它弥补了传统关系型数据库在这方面带来的复杂度,使得只需要简单的一个命令就可以完成一个复杂SQL任务,并且交、并、差操作在实际的业务场景中应用非常广泛,比如快速检索出具备一系列标签属性的一个集合,本篇文章将主要介绍对于求交集操作结果缓存的设计方案。

Redis命令

对于set类型,提供了sinter、sinterstore进行交集操作,对于sortedset,提供了zinter、zinterstore进行交集操作。命令十分便捷,对于不保存结果的方法sinter和zinter只需要输入待求交集的集合的key的数组就可以得到求交集后的结果集合,对于保存结果的方法,你可以将求交集后的结果集合保存到你指定key的一个集合中。

设计方案

目标

计算给定集合群的交集集合,并对计算过程中所产生的中间结果集合进行缓存,从而达到下次计算给定集合群的一些子群集时,先查询是否存在交集key,如果存在直接读取,避免重复计算。

原始方法(Only)

以下我们以Redis Java客户端库Jedis进行举例,Jedis提供了与Redis命令的一致接口方法,实现了交集操作,如下代码:

public Set<String> inter(String[] keys, String keycached){        int size = keys.length;        Jedis jedis = new Jedis("127.0.0.1");        if(size < 2){            try{                return jedis.smembers(keys[0]);            } finally {                jedis.close();            }        }        try{            jedis.sinterstore(keycached, keys);            return jedis.smembers(keycached);        } finally {            jedis.close();        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

原始方法的问题在于只对最终的交集key进行了缓存,简洁方便,但每次变更给定集合群时,都需要重新在此计算。

原始方法上的改造方案(All)

在原始方法上进行改造,我们可以在计算过程中依次增加计算集合群的集合数量,比如给定的集合群key{A,B,C,D},我们先计算A、B,保存一个{A,B}的交集结果,再依次计算A、B、C和A、B、C、D并对结果进行保存。
显然,这是个糟糕的方案,但确实完成了我们设定的目标,参考代码如下:

private Set<String> interByAll(String... keys){        Jedis jedis = new Jedis("127.0.0.1");        Set<String> value = null;        int interNum = 2;        for(int i = 0; i < (keys.length - 1); i++){            String keystored = "";            String[] keyintered = new String[interNum];            for(int j = 0; j < interNum; j++){                keystored += (keys[j] + "&");                keyintered[j] = keys[j];            }            if(jedis.sinterstore(keystored, keyintered) == 0){                jedis.sadd(keystored, "nocache");            }            if(interNum == keys.length){                value = jedis.smembers(keystored);            }            interNum++;        }        jedis.close();        return value;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

递归方案(Recursive)

根据上面糟糕的设计方案,你应该改进实现一种递归方案,递归方案的好处是你每次只求一对集合的交集,逐步完成对整个给定集合群的交集计算,计算过程如下图所示:

    private boolean isEnd = false;    private Set<String> value = null;    private Set<String> getKeysWithInner(String[] keys, String srckey, Jedis jedis, int i){        String key = null;        if(srckey == null){//表示为第一次求交集            srckey = keys[i++];            key = keys[i++];        } else {            key = keys[i++];        }        String keystored = srckey + "&" + key;//生成缓存key        if(jedis.sinterstore(keystored, srckey, key) == 0){            jedis.sadd(keystored, "nocache");        }        if(i == keys.length){ //当与最后一个key求交集后,返回结果,并跳出递归调用            value = jedis.smembers(keystored);            isEnd = true;        }        while(!isEnd){//递归调用,一对key集合求交集            getKeysWithInner(keys, keystored, jedis, i);        }        return value;    }    public Set<String> interByRecursive(String... keys){        int size = keys.length;        Jedis jedis = new Jedis("127.0.0.1");        if(size < 2){            try{                return jedis.smembers(keys[0]);            } finally {                jedis.close();            }        }        try{            return getKeysWithInner(keys, null, jedis, 0);        } finally {            isEnd = false;            jedis.close();        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

该方案的优势不仅仅是对计算过程进行了缓存,而且,每次都只是完成一对集合的计算,计算量显著降低。

Fork-Join方案(Fork-Join)

一写递归方案,你就可以直接想到使用Fork-Join框架进行改造,并使其并行化,计算过程如下图所示:

InterTask类:

public class InterTask extends RecursiveTask<String>{    private String[] keys = null;    private static final int THRESHOLD = 3;    public InterTask(String[] keys){        this.keys = keys;    }    private static String genKeyinnered(String... keys){        StringBuilder sb = new StringBuilder();        for(String key : keys){            sb.append(key);            sb.append("&");        }        return sb.toString().substring(0, sb.length() - 1);    }    @Override    protected String compute() {        int size = keys.length;        if(size < THRESHOLD){//当keys数组中元素数为2个或1个时,计算交集,并退出递归            if(size < 2){                return keys[0];            }            Jedis jedis = new Jedis("127.0.0.1");            try{                jedis.sinterstore(genKeyinnered(keys), keys[0], keys[1]);            } finally {                jedis.close();            }            return genKeyinnered(keys);        } else {            //取keys数组的中值进行分治算法            String[] leftkeys = new String[size / 2];            String[] rightkeys = new String[size - (size / 2)];            //按中值拆分keys数组            for(int i = 0; i < size; i++){                if(i < leftkeys.length){                    leftkeys[i] = keys[i];                } else {                    rightkeys[i - leftkeys.length] =  keys[i];                }            }            InterTask lefttask = new InterTask(leftkeys);            InterTask righttask = new InterTask(rightkeys);            lefttask.fork();            righttask.fork();            //取得从递归中返回的一对存储交集结果的key            String left = lefttask.join();            String right = righttask.join();            Jedis jedis = new Jedis("127.0.0.1");            try{                jedis.sinterstore(left + "&" + right, left, right);            } finally {                jedis.close();            }            return left + "&" + right;        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

这里运用了最基础的分治算法思想,逐步将一个大的给定集合拆解为若干个成对的集合进行交集计算。
调用方法:

public Set<String> interByForkJoin(String... keys){        Set<String> value = null;        Jedis jedis = new Jedis("127.0.0.1");        InterTask task = new InterTask(keys);        ForkJoinPool forkJoinPool = new ForkJoinPool();        Future<String> result = forkJoinPool.submit(task);        try {            String key = result.get();            value = jedis.smembers(key);        } catch (InterruptedException e) {            e.printStackTrace();        } catch (ExecutionException e) {            e.printStackTrace();        } finally {            jedis.close();        }        return value;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

测试

测试数据准备

我们这里准备100个集合,每个给定集合包含1000000个元素,参考如下代码:

    Jedis jedis = new Jedis("127.0.0.1");    jedis.flushAll();    String token = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";    Random rand = new Random();    String[] keys = new String[100];    for(int i = 0; i < 100; i++){        keys[i] = "ct" + i;        String[] values = new String[1000000];        for(int j = 0; j < 1000000; j++){            StringBuilder sb = new StringBuilder();            for(int k = 0;k < 2; k++){                sb.append(token.charAt(rand.nextInt(62)));            }            values[j] = sb.toString();        }        jedis.sadd(keys[i], values);    }    jedis.close();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

测试结果

我们分别对25、50、75、100的给定集合进行交集计算,测试结果如下:

我们可以清楚的看到,All方案是多么的糟糕,剔除All方案的结果:

总体来说Only方案和Recursive方案不分伯仲,但在相对较小的给定合集计算场景下,Recursive存在优势,而且其进行了计算过程结果的缓存。
对于Fork-Join方案表示比较遗憾,当然这里可以采用另外一种更优的分解算法完成并行过程,但是就Redis本身作为通过单线程epoll模型实现的异步IO来说,可能客户端的并行计算在服务端仍然被串行化处理,另外,分治算法拆分数组的时间损耗也不能忽略。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
什么是分布式锁?Redis实现分布式锁详解
利用redis + lua解决抢红包高并发的问题
经典:Redis分布式锁的正确实现方式
一不小心肝出了4W字的Redis面试教程
奉化制作银行流水
使用Redis,你必须知道的21个注意要点
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服