Redis 分页排序查询

Redis是一个高效的内存数据库,它支持包括String、List、Set、SortedSet和Hash等数据类型的存储,在Redis中通常根据数据的key查询其value值,Redis没有条件查询,在面对一些需要分页或排序的场景时(如评论,时间线),Redis就不太好不处理了。

前段时间在项目中需要将每个主题下的用户的评论组装好写入Redis中,每个主题会有一个topicId,每一条评论会和topicId关联起来,得到大致的数据模型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
    topicId: 'xxxxxxxx',
    comments: [
        {
            username: 'niuniu',
            createDate: 1447747334791,
            content: '在Redis中分页',
            commentId: 'xxxxxxx',
            reply: [
                {
                    content: 'yyyyyy'
                    username: 'niuniu'
                },
                ...
            ]
        },
        ...
    ]
}

将评论数据从MySQL查询出来组装好存到Redis后,以后每次就可以从Redis获取组装好的评论数据,从上面的数据模型可以看出数据都是key-value型数据,无疑要采用hash进行存储,但是每次拿取评论数据时需要分页而且还要按createDate字段进行排序,hash肯定是不能做到分页和排序的。

那么,就挨个看一下Redis所支持的数据类型:

1、String: 主要用于存储字符串,显然不支持分页和排序。
2、Hash: 主要用于存储key-value型数据,评论模型中全是key-value型数据,所以在这里Hash无疑会用到。
3、List: 主要用于存储一个列表,列表中的每一个元素按元素的插入时的顺序进行保存,如果我们将评论模型按createDate排好序后再插入List中,似乎就能做到排序了,而且再利用List中的LRANGE key start stop指令还能做到分页。嗯,到这里List似乎满足了我们分页和排序的要求,但是评论还会被删除,就需要更新Redis中的数据,如果每次删除评论后都将Redis中的数据全部重新写入一次,显然不够优雅,效率也会大打折扣,如果能删除指定的数据无疑会更好,而List中涉及到删除数据的就只有LPOP和RPOP这两条指令,但LPOP和RPOP只能删除列表头和列表尾的数据,不能删除指定位置的数据,所以List也不太适合
(转载的时候看了下,是有 LREM命令可以做到删除,但是LRANGE 似乎是一个耗时命令 O(N) )
4、Set: 主要存储无序集合,无序!排除。
5、SortedSet: 主要存储有序集合,SortedSet的添加元素指令ZADD key score member [[score,member]…]会给每个添加的元素member绑定一个用于排序的值score,SortedSet就会根据score值的大小对元素进行排序,在这里就可以将createDate当作score用于排序,SortedSet中的指令ZREVRANGE key start stop又可以返回指定区间内的成员,可以用来做分页,SortedSet的指令ZREM key member可以根据key移除指定的成员,能满足删评论的要求,所以,SortedSet在这里是最适合的(时间复杂度O(log(N))

所以,我需要用到的数据类型有SortSet和Hash,SortSet用于做分页排序,Hash用于存储具体的键值对数据,我画出了如下的结构图:

      在上图的SortSet结构中将每个主题的topicId作为set的key,将与该主题关联的评论的createDate和commentId分别作为set的score和member,commentId的顺序就根据createDate的大小进行排列。
      当需要查询某个主题某一页的评论时,就可主题的topicId通过指令zrevrange topicId (page-1)×10 (page-1)×10+perPage这样就能找出某个主题下某一页的按时间排好顺序的所有评论的commintId。page为查询第几页的页码,perPage为每页显示的条数。
当找到所有评论的commentId后,就可以把这些commentId作为key去Hash结构中去查询该条评论对应的内容。
这样就利用SortSet和Hash两种结构在Redis中达到了分页和排序的目的。


博主额外添加的实现算法:

[java] view plain copy
  1. @Test  
  2.     public void sortedSetPagenation(){  
  3.         for  ( int  i =  1 ; i <=  100 ; i+=10) {    
  4.             // 初始化CommentId索引 SortSet  
  5.             RedisClient.zadd("topicId", i, "commentId"+i);  
  6.             // 初始化Comment数据 Hash  
  7.             RedisClient.hset("Comment_Key","commentId"+i, "comment content .......");  
  8.         }    
  9.         // 倒序取 从0条开始取 5条 Id 数据  
  10.         LinkedHashSet<String> sets = RedisClient.zrevrangebyscore("topicId""80""1"05);  
  11.         String[] items = new String[]{};  
  12.         System.out.println(sets.toString());  
  13.         // 根据id取comment数据  
  14.         List<String> list = RedisClient.hmget("Comment_Key", sets.toArray(items));  
  15.         for(String str : list){  
  16.             System.out.println(str);  
  17.         }  
  18.     }  
工具类:
[java] view plain copy
  1. package com.util;  
  2.   
  3. import java.util.LinkedHashSet;  
  4. import java.util.List;  
  5.   
  6. import redis.clients.jedis.Jedis;  
  7. import redis.clients.jedis.JedisPool;  
  8. import redis.clients.jedis.JedisPoolConfig;  
  9.   
  10. /** 
  11.  * Redis 客户端集群版 
  12.  * 
  13.  * @author babylon 
  14.  * 2016-5-10 
  15.  */  
  16. public class RedisClient{  
  17.       
  18.     private static  JedisPool jedisPool;  
  19.       
  20.     static {  
  21.         JedisPoolConfig config = new JedisPoolConfig();  
  22.         config.setMaxTotal(Global.MAX_ACTIVE);     
  23.         config.setMaxIdle(Global.MAX_IDLE);  
  24.         config.setMaxWaitMillis(-1);  
  25.         config.setTestOnBorrow(Global.TEST_ON_BORROW);  
  26.         config.setTestOnReturn(Global.TEST_ON_RETURN);  
  27.         jedisPool = new JedisPool("redis://:"+Global.REDIS_SERVER_PASSWORD+"@"+Global.REDIS_SERVER_URL+":"+Global.REDIS_SERVER_PORT);  
  28. //      jedisPool = new JedisPool(config, Global.REDIS_SERVER_URL,  Integer.parseInt(Global.REDIS_SERVER_PORT), "zjp_Redis_224");  
  29.     }  
  30.   
  31.     public static  String set(String key, String value) {  
  32.         Jedis jedis = jedisPool.getResource();  
  33.         String result = jedis.set(key, value);  
  34.         jedis.close();  
  35.         return result;  
  36.     }  
  37.   
  38.     public static  String get(String key) {  
  39.         Jedis jedis = jedisPool.getResource();  
  40.         String result = jedis.get(key);  
  41.         jedis.close();  
  42.         return result;  
  43.     }  
  44.   
  45.     public static Long hset(String key, String item, String value) {  
  46.         Jedis jedis = jedisPool.getResource();  
  47.         Long result = jedis.hset(key, item, value);  
  48.         jedis.close();  
  49.         return result;  
  50.     }  
  51.   
  52.     public static  String hget(String key, String item) {  
  53.         Jedis jedis = jedisPool.getResource();  
  54.         String result = jedis.hget(key, item);  
  55.         jedis.close();  
  56.         return result;  
  57.     }  
  58.       
  59.     /** 
  60.      * Redis Hmget 命令用于返回哈希表中,一个或多个给定字段的值。 
  61.         如果指定的字段不存在于哈希表,那么返回一个 nil 值。 
  62.      * @param key 
  63.      * @param item 
  64.      * @return 一个包含多个给定字段关联值的表,表值的排列顺序和指定字段的请求顺序一样。 
  65.      */  
  66.     public static  List<String> hmget(String key, String... item) {  
  67.         Jedis jedis = jedisPool.getResource();  
  68.         List<String> result = jedis.hmget(key, item);  
  69.         jedis.close();  
  70.         return result;  
  71.     }  
  72.   
  73.     public static  Long incr(String key) {  
  74.         Jedis jedis = jedisPool.getResource();  
  75.         Long result = jedis.incr(key);  
  76.         jedis.close();  
  77.         return result;  
  78.     }  
  79.   
  80.     public static  Long decr(String key) {  
  81.         Jedis jedis = jedisPool.getResource();  
  82.         Long result = jedis.decr(key);  
  83.         jedis.close();  
  84.         return result;  
  85.     }  
  86.   
  87.     public static Long expire(String key, int second) {  
  88.         Jedis jedis = jedisPool.getResource();  
  89.         Long result = jedis.expire(key, second);  
  90.         jedis.close();  
  91.         return result;  
  92.     }  
  93.   
  94.     public static Long ttl(String key) {  
  95.         Jedis jedis = jedisPool.getResource();  
  96.         Long result = jedis.ttl(key);  
  97.         jedis.close();  
  98.         return result;  
  99.     }  
  100.   
  101.     public static Long hdel(String key, String item) {  
  102.         Jedis jedis = jedisPool.getResource();  
  103.         Long result = jedis.hdel(key, item);  
  104.         jedis.close();  
  105.         return result;  
  106.     }  
  107.   
  108.     public static Long del(String key) {  
  109.         Jedis jedis = jedisPool.getResource();  
  110.         Long result = jedis.del(key);  
  111.         jedis.close();  
  112.         return result;  
  113.     }  
  114.       
  115.     public static Long rpush(String key, String... strings) {  
  116.         Jedis jedis = jedisPool.getResource();  
  117.         Long result = jedis.rpush(key, strings);  
  118.         jedis.close();  
  119.         return result;  
  120.     }  
  121.   
  122.     /** 
  123.      * Redis Lrange 返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。  
  124.      * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 
  125.      * @param string 
  126.      * @param start 
  127.      * @param end 
  128.      * @return 
  129.      */  
  130.     public static List<String> lrange(String key, int start, int end) {  
  131.         Jedis jedis = jedisPool.getResource();  
  132.         List<String> result = jedis.lrange(key, start, end);  
  133.         jedis.close();  
  134.         return result;  
  135.     }  
  136.   
  137.     /** 
  138.      * 从列表中从头部开始移除count个匹配的值。如果count为零,所有匹配的元素都被删除。如果count是负数,内容从尾部开始删除。 
  139.      * @param string 
  140.      * @param string2 
  141.      * @param i 
  142.      */  
  143.     public static Long lrem(String key, Long count, String value) {  
  144.         Jedis jedis = jedisPool.getResource();  
  145.         Long result = jedis.lrem(key, count, value);  
  146.         jedis.close();  
  147.         return result;  
  148.     }  
  149.   
  150.     /** 
  151.      * Redis Zadd 命令用于将一个或多个成员元素及其分数值加入到有序集当中。 
  152.         如果某个成员已经是有序集的成员,那么更新这个成员的分数值,并通过重新插入这个成员元素,来保证该成员在正确的位置上。 
  153.         分数值可以是整数值或双精度浮点数。 
  154.         如果有序集合 key 不存在,则创建一个空的有序集并执行 ZADD 操作。 
  155.         当 key 存在但不是有序集类型时,返回一个错误。 
  156.      * @param string 
  157.      * @param i 
  158.      * @param string2 
  159.      * @return 被成功添加的新成员的数量,不包括那些被更新的、已经存在的成员。 
  160.      */  
  161.     public static Long zadd(String key, double score, String member) {  
  162.         Jedis jedis = jedisPool.getResource();  
  163.         Long result = jedis.zadd(key, score, member);  
  164.         jedis.close();  
  165.         return result;  
  166.     }  
  167.       
  168.     /** 
  169.      * Redis Zrevrangebyscore 返回有序集中指定分数区间内的所有的成员。有序集成员按分数值递减(从大到小)的次序排列。 
  170.         具有相同分数值的成员按字典序的逆序(reverse lexicographical order )排列。 
  171.         除了成员按分数值递减的次序排列这一点外, ZREVRANGEBYSCORE 命令的其他方面和 ZRANGEBYSCORE 命令一样。 
  172.      * @param key 
  173.      * @param max 
  174.      * @param min 
  175.      * @param offset 
  176.      * @param count 
  177.      * @return 指定区间内,带有分数值(可选)的有序集成员的列表。 
  178.      */  
  179.     public static LinkedHashSet<String> zrevrangebyscore(String key, String max, String min, int offset, int count){  
  180.         Jedis jedis = jedisPool.getResource();  
  181.         LinkedHashSet<String> result = (LinkedHashSet<String>) jedis.zrevrangeByScore(key, max, min, offset, count);  
  182.         jedis.close();  
  183.         return result;  
  184.     }  
  185.   

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值