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();}}原始方法的問題在于只對最終的交集key進行了緩存,簡潔方便,但每次變更給定集合群時,都需要重新在此計算。
原始方法上的改造方案(All)
在原始方法上進行改造,我們可以在計算過程中依次增加計算集合群的集合數量,比如給定的集合群key{A,B,C,D},我們先計算A、B,保存一個{A,B}的交集結果,再依次計算A、B、C和A、B、C、D并對結果進行保存。?
顯然,這是個糟糕的方案,但確實完成了我們設定的目標,參考代碼如下:
?
遞歸方案(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;//生成緩存keyif(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();}}?
該方案的優勢不僅僅是對計算過程進行了緩存,而且,每次都只是完成一對集合的計算,計算量顯著降低。
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);}@Overrideprotected 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();//取得從遞歸中返回的一對存儲交集結果的keyString 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;}}}?
這里運用了最基礎的分治算法思想,逐步將一個大的給定集合拆解為若干個成對的集合進行交集計算。?
調用方法:
?
測試
測試數據準備
我們這里準備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();測試結果
我們分別對25、50、75、100的給定集合進行交集計算,測試結果如下:
我們可以清楚的看到,All方案是多么的糟糕,剔除All方案的結果:
總體來說Only方案和Recursive方案不分伯仲,但在相對較小的給定合集計算場景下,Recursive存在優勢,而且其進行了計算過程結果的緩存。?
對于Fork-Join方案表示比較遺憾,當然這里可以采用另外一種更優的分解算法完成并行過程,但是就Redis本身作為通過單線程epoll模型實現的異步IO來說,可能客戶端的并行計算在服務端仍然被串行化處理,另外,分治算法拆分數組的時間損耗也不能忽略。
轉載:https://blog.csdn.net/xreztento/article/details/53289193
總結
以上是生活随笔為你收集整理的Redis实现求交集操作结果缓存的设计方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mongo数据库CRUD
- 下一篇: 技术专家:为什么我们最终选择Apache