Redis是一个高效的内存数据库,它支持包括String、List、Set、SortedSet和Hash等数据类型的存储,在Redis中通常根据数据的key查询其value值,Redis没有条件查询,在面对一些需要分页或排序的场景时(如评论,时间线),Redis就不太好不处理了。
前段时间在项目中需要将每个主题下的用户的评论组装好写入Redis中,每个主题会有一个topicId,每一条评论会和topicId关联起来,得到大致的数据模型如下:
12345678910111213141516171819 | { 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", 0, 5);
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.
186. }