简介
Redis是一个开源的、高性能的基于键值对的缓存与存储系统,通过提供多种键值数据类型开适应不同场景下的缓存与存储需求。同时redis的诸多高层级功能使其可以胜任消息队列、任务队列等不同角色。
使用场景
用作数据库
用作缓存
用作队列系统
安装
一:使用微软发布的可在windows运行的redis分支
https://github.com/MicrosoftArchive/redis/releases
redis-server.exe : redis 服务器,地洞redis即运行redis-server;
redis-cli: redis自带的redis命令行客户端,是学习redis的重要工具;
启动和停止REDIS
命令行方式:
redis-cli执行时会自动按照默认配置(服务器地址:127.0.0.1,端口号:6379)连接redis,通过-h和-p参数可以自定义地址和端口号:
redis-cli –h 127.0.0.1 –p 6279
redis提供了PING命令来测试客户端与redis的连接是否正常,如果连接正常会收到回复PONG。
停止redis:
考虑到redis有可能正在将内存中的数据同步到硬盘中,强行终止redis进程可能会导致数据丢失。还真能却停止redis的方式应该是向redis发送SHUTDOWN命令:
SHUTDOWN
当redis收到SHUTDOWN命令后,会先断开所有客户端连接,然后根据配置执行持久化,最后完成退出。
二:通过使用cygwin软件完成-cygwin能够在windows中模拟Linux系统环境
1、下载cygwin http://www.cygwin.com/
2、下载redis http://download.redis.io/redis-stable.tar.gz
略
REDIS配置
Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf。
你可以通过 CONFIG 命令查看或设置配置项。
语法
Redis CONFIG 命令格式如下:
redis 127.0.0.1:6379> CONFIG GET CONFIG_SETTING_NAME
实例
redis 127.0.0.1:6379> CONFIG GET loglevel
1) "loglevel"
2) "notice"
使用 * 号获取所有配置项:
实例
redis 127.0.0.1:6379> CONFIG GET *
编辑配置
你可以通过修改 redis.conf 文件或使用 CONFIG set 命令来修改配置。
语法
CONFIG SET 命令基本语法:
redis 127.0.0.1:6379> CONFIG SET CONFIG_SETTING_NAME NEW_CONFIG_VALUE
实例
redis 127.0.0.1:6379> CONFIG SET loglevel "notice"
OK
redis 127.0.0.1:6379> CONFIG GET loglevel
1) "loglevel"
2) "notice"
参数说明
redis.conf 配置项说明如下:
1. Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize no
2. 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定
pidfile /var/run/redis.pid
3. 指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字
port 6379
4. 绑定的主机地址
bind 127.0.0.1
5.当 客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
timeout 300
6. 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose
loglevel verbose
7. 日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null
logfile stdout
8. 设置数据库的数量,默认数据库为0,可以使用SELECT <dbid>命令在连接上指定数据库id
databases 16
9. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
save <seconds> <changes>
Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。
10. 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
rdbcompression yes
11. 指定本地数据库文件名,默认值为dump.rdb
dbfilename dump.rdb
12. 指定本地数据库存放目录
dir ./
13. 设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
slaveof <masterip> <masterport>
14. 当master服务设置了密码保护时,slav服务连接master的密码
masterauth <master-password>
15. 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH <password>命令提供密码,默认关闭
requirepass foobared
16. 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
maxclients 128
17. 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
maxmemory <bytes>
18. 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
appendonly no
19. 指定更新日志文件名,默认为appendonly.aof
appendfilename appendonly.aof
20. 指定更新日志条件,共有3个可选值:
no:表示等操作系统进行数据缓存同步到磁盘(快)
always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
everysec:表示每秒同步一次(折衷,默认值)
appendfsync everysec
21. 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)
vm-enabled no
22. 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
vm-swap-file /tmp/redis.swap
23. 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0
vm-max-memory 0
24. Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值
vm-page-size 32
25. 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。
vm-pages 134217728
26. 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
vm-max-threads 4
27. 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
glueoutputbuf yes
28. 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
hash-max-zipmap-entries 64
hash-max-zipmap-value 512
29. 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)
activerehashing yes
30. 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件
include /path/to/local.conf
Redis命令行客户端
发送命令
两种方式:
- 将命令作为redis-cli的参数执行:
- 不附带参数运行redis-cli,这样会进入交互模式,可以自由输入命令:(适用于要输入多条命令)
Redis数据结构简介
要学会redis就要先掌握redis的键值数据类型和相关的命令,这些内容是redis的基础。
一 字符串 STRING
Redis的字符串就是一个由字节组成的序列。一个字符串类型允许存储的数据的最大容量是512MB。
字符串可以存储一下三种类型的值。
字节串(byte string)
整数
浮点数
命令:
SET 设置存储在给定键中的值
GET获取存储在给定键中的值
DEL删除存储在给定键中的值(这个命令可以用于所有类型)
自增命令与自减命令
INCR INCR key-name 将键存储的值加上1(当要操作的键不存在时会默认键值为0,所以第一次递增后的结果是1,。当键值不是整数时会提示错误)
DECR DECR key-name 将键存储的值减去1
INCRBY INCRBY key-name amount 将键存储的值加上整除amount
DECRBY DECRBY key-name amount 将键存储的值减去整除amount
INCRBYFLOAT INCRBYFLOAT key-name amount 将键存储的值加上浮点数amount redis2.6 版本及以上适用
处理字符串字串和二进制位的命令
APPEND APPENG key-name value 将值追加到给定键当前存储的值的末尾
GETRANGE
SETRANGE
GETBIT
SETBIT
BITCOUNT
BITOP BITOP operation destkey key [key …]
*
STRLEN STRLEN key 返回键值的长度,如果键不存在则返回0
MGET 同时获得多个键值
MSET 同时获得多个键值
Append命令的第二个参数加了引号,原因是该参数包含空格,在redis-cli中输入需要引号已是区分。
二 列表 LIST
可以存储一个有序的字符串列表。列表类型内部使用的是双向链表实现的,所以向列表两端添加元素的时间复杂度为O(1),获取越接近两端的元素速度就越快。
Redis列表起始索引为0。
列表是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会较慢,所以它更加适合实现如“新鲜事”或“日志”这样很少访问中间元素的应用。
命令:
推入:
LPUSH 将元素推入列表的左端
RPUSH 将元素推入列表的右端
弹出
LPOP 从列表左端弹出元素
RPOP从列表右端弹出元素
LSET LSET key index value 设置指定索引的元素值
LINDEX 获取列表在给定位置上的单个元素
LRANGE 获取列表在给定范围上的所有值
从语义上来说,列表的左端为开头,右端为结尾
使用0为范围的起始索引,-1为范围的结束索引,可以取出列表包含的所有元素
LTRIM LTRIM key-name start end 对列表进行修改,只保留从start偏移量到end偏移量范围内的元素,其中偏移量为start和偏移量为end的元素也会被保留。
LLEN LLEN key 获取列表中元素的个数,点键不存在时LLEN会返回0 (时间复杂度为O(1))
LREM LREM key count value 删除列表中前count个值为value的元素
(count>0 从左侧删,count<0从右侧删,count=0删除所有)
LINSERT LINSERT key BEFORE|AFTER pivot value 向列表中插入元素
LINSERT命令首先会在列表中从左到右查找值为pivot的元素,然后根据第二个参数是BEFORE|AFTER来决定将value插入到该元素的前面还是后面。
RPOPLPUSH RPOPLPUSH source destination 将元素从一个列表转到另一个列表
三 集合 SET
Redis的集合和列表都可以存储多个字符串,他们之间的不同在于,列表可以存储多个相同的字符串,二而集合则通过散列表来保证自己存储的每个字符串都是不同的。(不同且没有顺序 )
Redis集合使用无序方式存储元素。
命令:
SADD 将给定元素添加至集合(键不存在会自动创建)
SREM 返回集合包含的所有元素
SISMEMBER 检查给定元素是否存在于集合中(时间复杂度为O(1))
SMEMBERS 返回集合包含的所有元素
SCARD SCARD key-name 返回集合包含的元素的数量
SRANDMEMBER SRANDMEMBER key-name [count] 从集合里面随机地返回一个或多个元素。当count为正数时,命令返回的随机元素不会重复;当count为负数时,命令返回的随机元素可能会出现重复;
SPOP SPOP key-name 随机地移除集合中的一个元素,并返回被移除的元素
SMOVE SMOVE source-key dest-key item 成功返回1,否则返回0
用于组合和处理多个集合的redis命令
(实战 P45)
差集
SDIFF
SDIFFSTORE
交集
SINTER
SINTERSTORE
并集
SUNION
SUNIONSTORE
四 散列 HASH
Redis的散列可以存储多个键值对之间的映射。字段值只能是字符串,不支持其他数据类型。一个散列类型键可以包含2^32-1个字段。
命令:
HSET 在散列里面关联起给定的键值对(新建与更新,键不存在自动创建)
HGET 获取指定散列键的值
HGETALL 获取散列包含的所有键值对
HDEL 如果给定键存在于散列里面,那么移除这个键
HMSET 为散列里面的一个或多个键设置值
HMGET 从散列里获取一个或多个键值对
HLEN HLEN key-name 返回散列包含的键值对数量
散列的更高级特性
HEXISTS HEXISTS key-name value 检查给定键是否存在于散列中
HKEYS HKEYS key-name 获取散列包含的所有键
HVALS HVALS key-name 获取散列包含的所有值
HINCRBY HINCRBY key-name key increment 将键key存储的值加上整数increment
HINCRBYFLOAT HINCRBYFLOAT key-name key increment 将键key存储的值加上浮点数
Increment
HSETNX HSETNX key field value 当字段不存在时赋值
(与HSET命令类型,区别在于如果字段已经存在,HSETNX命令将不执行任何操作)
五 有序集合 ZSET
有序集合和散列一样,都用于存储键值对:有序集合的键被称为成员(member),每个成员都是各不相同的;而有序集合的值则被称为分值(score),分值必须为浮点数。
(虽然集合中每个元素都是不同的,但是他们的分数确可以相同)
有序集合是redis中唯一一个既可以根据成员访问元素,又可以根据分值以及分值的排列顺序来访问元素的结构。
又不集合是通过散列表和跳表实现的,所以即使读取位于中间部分的数据速度也很快(时间复杂度O(log(N)))
命令:
ZADD 将一个带有给定分值的成员添加到有序集合里面
zadd key [NX|XX] [CH] [INCR] score member [score member…]
ZRANGE 根据元素在有序排列中所处的位置,从有序集合里面获取多个元素从(从小到大)
zrange key start stop [WITHSCORES]
ZREANGEBYSCORE 获取有序集合在给定范围内的所有元素(从小到大)
zrangebyscore key min max [WITHSCORES] [LIMIT offset count]
ZREM 如果给定成员存在于有序集合,那么移除这个元素
zrem key member [member…]
ZCARD ZCARD key-name 返回有序集合包含的成员数量
ZINCRBY ZINCRBY key-name increment member 将member成员的分值加上increment
ZCOUNT ZCOUNT key-name min max 返回分值介于min-max之间的成员数量(主要用于计算分值在给定范围内的成员数量)
ZRANK ZRANK key-name member 返回成员member在有序集合中的排名(从小到大)
ZSCORE ZSCORE key-name member 返回成员member的分值
ZREVRANK (从大到小)
ZREVRANGE(从大到小)
ZREVRANGEBYSCORE (从大到小)
ZREMRANGEBYRANK ZREMRANGEBYRANK key-name start stop 移除有序集合中排名介于start和stop之间的所有成员
ZREMRANGEBYSCORE ZREMRANGEBYSCORE key-name min max 移除有序集合中分值介于min-max之间的所有成员
ZINTERSTORE ZINTERSTORE dest-key key-count key [key …] [WEIGHTS weight [weight …]] [AGGREGATE SUM|MIN|MAX] 对给定的有序集合执行类似于集合的交集运算 (默认sum)
ZUNIONSTORE ZUNIONSTORE dest-key key-count key [key …] [WEIGHTS weight [weight …]] [AGGREGATE SUM|MIN|MAX] 对给定的有序集合执行类似于集合的并集运算
六 进阶
1. 排序
SORT SORT source-key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern …]] [ASC|DESC] [ALPHA] [STORE dest-key] 根据给定的选项,对输入列表、集合或者有序集合进行排序,然后返回或者存储排序的结果
默认:根据数字大小对元素进行排序
ALPHA:true 根据字母表顺序对元素进行排序
[LIMIT offset count]:跳过前offset个元素并获取之后的count个元素。
[BY pattern]:根据对象的某个属性进行排序,只能根据一个属性排序
[GET pattern …]:返回指定的键值,可返回多个
2. 基本的redis事务
Redis中的事务是一组命令的集合。事务同命令一样都是redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行。
为了对相同或者不同类型的多个键执行操作,redis有5个命令可以让用户在不被打断的情况下对多个键执行操作,分别是:WATCH、MULTI、EXEC、UNWATCH、DISCARD;
Redis基本事务需要用到MULTI命令和EXEC命令,这种事务可以让一个客户端在不被其他客户端打断的情况下执行多个命令。在redis里面,被MULTI命令和EXEC命令保卫的所有命令会一个接一个地执行,直到所有命令都执行完毕为止。当一个事务执行完毕之后,redis才会处理其他客户端的命令。
Redis要在接受到EXEC命令之后,才会执行那些位于MULTI和EXEC之间的入队命令。
如果在发送exec命令前客户端断线了,则reis会清空事务队列,事务中的所有命令都不会执行。
错误处理:
1)、语法错误:命令不存在或者命令参数的个数不对。
只要有一个命令有语法错误,执行EXEC命令后redis就会直接返回错误,连语法正确的也不执行。
2)、运行错误:指令执行时出现的错误。
命令执行前无法发现,所以事务中出现运行错误,其他的命令依然会执行。
WATCH:
WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。执行EXEC后取消对所有键的监控。
如果使用watch命令检测了一个拥有过期时间的键,该键时间到期自动删除并不会被wtach命令认为该键被改变。
3. 键的过期时间
在使用redis存储数据的时候,有些数据可能在某个时间点之后就不在有用了,用户可以通过redis的过期时间(expire)特性来让一个键在给定的时限(timeout)之后自动被删除。
用于处理过期时间的redis命令
PERSIST PERSIST key-name 移除键的过期时间
(使用SET或GETSET命令为键复制也会同时清除键的过期时间,其他对键值操作的命令均不会影响键的过期时间)
TTL TTL key-name 查看给定键距离过期还有多少秒(若没有设置过期时间,则返回-1,当键不存在时,返回-2)
EXPIRE EXPIRE key-name seconds 让给定键在指定的秒数之后过期
EXPIREAT EXPIREAT key-name timestamp 将给定键的过期时间设置为给定的UNIX时间戳
PTTL PTTL key-name 查看给定键距离过期还有多少毫秒
PEXPIRE PEXPIRE key-name seconds 让给定键在指定的毫秒数之后过期
PEXPIREAT PEXPIREAT key-name timestamp 将一个毫秒级精度的UNIX时间戳这只为给定键的过期时间
4. 消息通知
任务队列
“发布/订阅”模式(publish/subscribe):
包含两个角色:发布者(publish)和订阅者(subscribe);订阅者可以订阅一个或若干个频道(channel),而发布者可以向指定的频道发送消息,所有订阅此频道的订阅者都会受到消息。
Redis A> subscribe channel.1
Redis B>publish channel.1 hi!
Redis A>
- “message”
- “channel.1”
- “hi!”
按照规则订阅:
Redis C>psubscribe channel.?* glob风格通配符
Punsubscribe 只能退订psubscribe订阅的频道
unsubscribe 只能退订subscribe订阅的频道
5. 管道
客户端和redis使用TCP协议连接,不论是客户端向redis发送命令还是redis向客户端返回命令的执行结果,都需要经过网络传输,者两部分的总耗时称为往返时延。
Redis的底层通信协议对管道(pipelining)提供了支持。管道可以一次发送多条命令并在执行完成奇偶一次性将结果返回,当一组命令中每条命令都不依赖于之前命令的执行结果时就可以将这组命令一起通过管道发出。管道通过减少客户端与redis的通信次数来实现降低往返时延累计值的目的。
6. 节省空间
- 精简键名和键值
- 内部编码优化
Maven项目引用redis jar包
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.7.2.RELEASE</version>
</dependency>
使用jedis
package redisInJava;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ZParams;
public class Chapter01 {
private static final int ONE_WEEK_IN_SECONDS = 7 * 86400;
private static final int VOTE_SCORE = 432;
private static final int ARTICLES_PER_PAGE = 25;
public static final void main(String[] args) {
new Chapter01().run();
}
public void run() {
Jedis conn = new Jedis("127.0.0.1"); // IP(默认端口)
conn.select(15); // 选择数据库
// 发布文章
String articleId = postArticle(
conn, "username", "A title", "http://www.google.com");
System.out.println("We posted a new article with id: " + articleId);
System.out.println("Its HASH looks like:");
Map<String,String> articleData = conn.hgetAll("article:" + articleId);
for (Map.Entry<String,String> entry : articleData.entrySet()){
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
System.out.println();
articleVote(conn, "other_user", "article:" + articleId);
String votes = conn.hget("article:" + articleId, "votes");
System.out.println("We voted for the article, it now has votes: " + votes);
assert Integer.parseInt(votes) > 1;
System.out.println("The currently highest-scoring articles are:");
List<Map<String,String>> articles = getArticles(conn, 1);
printArticles(articles);
assert articles.size() >= 1;
addGroups(conn, articleId, new String[]{"new-group"});
System.out.println("We added the article to a new group, other articles include:");
articles = getGroupArticles(conn, "new-group", 1);
printArticles(articles);
assert articles.size() >= 1;
}
public String postArticle(Jedis conn, String user, String title, String link) {
String articleId = String.valueOf(conn.incr("article:"));
String voted = "voted:" + articleId;
conn.sadd(voted, user);
conn.expire(voted, ONE_WEEK_IN_SECONDS);
long now = System.currentTimeMillis() / 1000;
String article = "article:" + articleId;
HashMap<String,String> articleData = new HashMap<String,String>();
articleData.put("title", title);
articleData.put("link", link);
articleData.put("user", user);
articleData.put("now", String.valueOf(now));
articleData.put("votes", "1");
conn.hmset(article, articleData);
conn.zadd("score:", now + VOTE_SCORE, article);
conn.zadd("time:", now, article);
return articleId;
}
public void articleVote(Jedis conn, String user, String article) {
long cutoff = (System.currentTimeMillis() / 1000) - ONE_WEEK_IN_SECONDS;
if (conn.zscore("time:", article) < cutoff){
return;
}
String articleId = article.substring(article.indexOf(':') + 1);
if (conn.sadd("voted:" + articleId, user) == 1) {
conn.zincrby("score:", VOTE_SCORE, article);
conn.hincrBy(article, "votes", 1);
}
}
public List<Map<String,String>> getArticles(Jedis conn, int page) {
return getArticles(conn, page, "score:");
}
public List<Map<String,String>> getArticles(Jedis conn, int page, String order) {
int start = (page - 1) * ARTICLES_PER_PAGE;
int end = start + ARTICLES_PER_PAGE - 1;
Set<String> ids = conn.zrevrange(order, start, end);
List<Map<String,String>> articles = new ArrayList<Map<String,String>>();
for (String id : ids){
Map<String,String> articleData = conn.hgetAll(id);
articleData.put("id", id);
articles.add(articleData);
}
return articles;
}
public void addGroups(Jedis conn, String articleId, String[] toAdd) {
String article = "article:" + articleId;
for (String group : toAdd) {
conn.sadd("group:" + group, article);
}
}
public List<Map<String,String>> getGroupArticles(Jedis conn, String group, int page) {
return getGroupArticles(conn, group, page, "score:");
}
public List<Map<String,String>> getGroupArticles(Jedis conn, String group, int page, String order) {
String key = order + group;
if (!conn.exists(key)) {
ZParams params = new ZParams().aggregate(ZParams.Aggregate.MAX);
conn.zinterstore(key, params, "group:" + group, order);
conn.expire(key, 60);
}
return getArticles(conn, page, key);
}
private void printArticles(List<Map<String,String>> articles){
for (Map<String,String> article : articles){
System.out.println(" id: " + article.get("id"));
for (Map.Entry<String,String> entry : article.entrySet()){
if (entry.getKey().equals("id")){
continue;
}
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
}
}
}