Redis是一个用ANSI C语言编写的,基于内存并且可以持久化的日志型、高性能key_value数据库。它通常被称为数据结构服务器,因为其存储的value可以是字符串、哈希、列表、集合和有序集合等类型。
一、Redis与其他key_value缓存产品对比:
相同点:
- 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- 不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- 支持数据的备份,即master-slave模式的数据备份。
不同点:
- Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
- Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
Reids的优势
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
二、Redis的安装
2.1 下载
下载地址:http://redis.io/download,下载最新稳定版本。
2.2 安装
将下载的压缩包放至安装目录,然后解压、编译。
$ tar -xvf redis-5.0.5.tar
$ cd redis-5.0.5
$ make
make完后,安装目录下会出现编译后的redis服务程序redis-server,还有用于测试的客户端程序redis-cli,两个程序位于安装目录 src 目录下。
2.3 配置
在Redis的安装目录里面,有一个redis.conf文件,编辑文件,根据实际需求进行配置,配置项说明如下:
2.4 启动
进入src目录下,启动Redis服务端:
$ cd src
$ ./redis-server ../redis.conf
启动命令后面的参数是指定的配置文件。redis.conf是默认的配置文件(在启动时可以不指定),我们也可以通过客户端程序redis-cli来修改我们的配置。
在src目录下,启动Redis客户端程序:
$ ./redis-cli
通过config命令查看或设置配置选项:
127.0.0.1:6379> config get *
1) "dbfilename"
2) "dump.rdb"
3) "requirepass"
4) ""
5) "masterauth"
6) ""
7) "cluster-announce-ip"
8) ""
9) "unixsocket"
10) ""
...
127.0.0.1:6379> config set loglevel notice
OK
127.0.0.1:6379> config get loglevel
1) "loglevel"
2) "notice"
三、基本数据类型的使用
3.1 String类型
string 是 redis 最基本的类型,一个 key 对应一个 value。它的值是二进制安全的,也就是说 redis 的 string 可以包含任何数据,比如jpg图片或者序列化的对象。string 类型的值最大能存储 512MB。string 的存取用的是get 和 set 命令:
127.0.0.1:6379> set java "a great language"
OK
127.0.0.1:6379> get java
"a great language"
3.2 Hash类型
Redis hash 是一个键值(key=>value)对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,每个 hash 可以存储 2^32 -1 键值对(40多亿)。hash类型的存取使用hmset和hget命令:
127.0.0.1:6379> hmset myhash field1 "value1" field2 "value2"
OK
127.0.0.1:6379> hget myhash field1
"value1"
127.0.0.1:6379> hget myhash field2
"value2"
3.3 List类型
Redis 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)。每个列表最多可存储 2^32 - 1 个元素 (40多亿)。存取使用lpush和lrange 命令:
127.0.0.1:6379> lpush mylist java
(integer) 1
127.0.0.1:6379> lpush mylist c
(integer) 2
127.0.0.1:6379> lpush mylist php python c++ scala
(integer) 6
127.0.0.1:6379> lrange mylist 0 10
1) "scala"
2) "c++"
3) "python"
4) "php"
5) "c"
6) "java"
3.3 Set类型
Redis的Set是string类型的无序集合。每个集合可存储 2^32 - 1个成员(40多亿)。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。使用sadd添加一个 string 元素到 key 对应的 set 集合中,成功返回1,如果元素已经在集合中返回 0,如果 key 对应的 set 不存在则返回错误,smembers获取集合元素:
127.0.0.1:6379> sadd myset hadoop
(integer) 1
127.0.0.1:6379> sadd myset zookeeper
(integer) 1
127.0.0.1:6379> sadd myset hive
(integer) 1
127.0.0.1:6379> sadd myset hbase
(integer) 1
127.0.0.1:6379> sadd myset spark
(integer) 1
127.0.0.1:6379> sadd myset hive
(integer) 0
127.0.0.1:6379> smembers myset
1) "hive"
2) "zookeeper"
3) "hbase"
4) "hadoop"
5) "spark"
3.3 ZSet类型
Redis 有序集合zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。使用zadd命令添加元素到集合:zadd key score member 。如果元素在集合中存在则更新对应score。使用zrange输出结果。
127.0.0.1:6379> zadd myzset 0 hive
(integer) 1
127.0.0.1:6379> zadd myzset 5 hadoop
(integer) 1
127.0.0.1:6379> zadd myzset 3 hbase
(integer) 1
127.0.0.1:6379> zadd myzset 2 zookeeper
(integer) 1
127.0.0.1:6379> zadd myzset 6 spark
(integer) 1
127.0.0.1:6379> zrange myzset 0 10
1) "hive"
2) "zookeeper"
3) "hbase"
4) "hadoop"
5) "spark"
127.0.0.1:6379> zrange myzset 0 10 withscores
1) "hive"
2) "0"
3) "zookeeper"
4) "2"
5) "hbase"
6) "3"
7) "hadoop"
8) "5"
9) "spark"
10) "6"
从输出结果可以看到确实是有序的集合。
四、Redis Java API的使用
Redis Java API 基本和命令差不多,都有对应的方法来操作。
package com.lzb.hdfs.redis;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class RedisHelper {
private Jedis jedis;
public static void main(String[] args) {
RedisHelper redisHelper = new RedisHelper();
redisHelper.testString();
redisHelper.testHash();
redisHelper.testList();
redisHelper.testSet();
redisHelper.testZSet();
}
public RedisHelper() {
jedis = new Jedis("192.168.218.106");
}
public void testString(){
String result = jedis.set("java", "a great laguage");
String value = jedis.get("java");
System.out.println(result);
System.out.println(value);
}
public void testHash(){
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("field1","value1");
hashMap.put("field2","value2");
hashMap.put("field3","value3");
String result = jedis.hmset("myhash",hashMap);
System.out.println(result);
String value = jedis.hget("myhash", "field1");
System.out.println(value);
Map<String, String> myHash = jedis.hgetAll("myhash");
Set<Map.Entry<String, String>> entries = myHash.entrySet();
for (Map.Entry<String, String> entry: entries) {
System.out.println(entry.getKey()+"==>" + entry.getValue());
}
}
public void testList(){
Long lpush = jedis.lpush("mylist", "java", "c", "c++", "php", "python", "scala");
System.out.println(lpush);
List<String> myList = jedis.lrange("mylist", 0, 10);
for (String str : myList) {
System.out.println(str);
}
}
public void testSet(){
Long sadd = jedis.sadd("myset", "zookeeper", "hadoop", "hive", "hbase", "spark");
System.out.println(sadd);
Set<String> mySet = jedis.smembers("myset");
for (String str : mySet) {
System.out.println(str);
}
}
public void testZSet(){
jedis.zadd("myzset",0,"zookeeper");
jedis.zadd("myzset",5,"hadoop");
jedis.zadd("myzset",4,"hive");
jedis.zadd("myzset",2,"hbase");
jedis.zadd("myzset",6,"spark");
Set<String> myZSet = jedis.zrange("myzset", 0, 10);
//Set<String> myZSet = jedis.zrangeWithScores("myzset", 0, 10);
for (String str : myZSet) {
System.out.println(str);
}
}
}
执行结果:
/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/bin/java...
OK
a great laguage
OK
value1
field3==>value3
field2==>value2
field1==>value1
6
scala
python
php
c++
c
java
5
hadoop
hive
zookeeper
hbase
spark
zookeeper
hbase
hive
hadoop
spark
Process finished with exit code 0
五、事务
Redis 事务可以一次执行多个命令,它的执行过程及特点如下:
1. 以 MULTI 命令开始一个事务, 然后将多个命令入队到事务中,这些命令操作在发送 EXEC 命令之前被放入缓存队列。
2. 当收到EXEC命令后触发事务的执行,事务中任意命令执行失败,其余的命令依然被执行。
3. 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
下面是一个事务的例子:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379>
127.0.0.1:6379> set book-name "Guidelines for rehabilitation of cervical spondylosis"
QUEUED
127.0.0.1:6379> get book-name
QUEUED
127.0.0.1:6379> sadd language "c++" "python" "scala" "java"
QUEUED
127.0.0.1:6379> smembers language
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) "Guidelines for rehabilitation of cervical spondylosis"
3) (integer) 4
4) 1) "c++"
2) "java"
3) "scala"
4) "python"
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
六、管道pipeline
Redis客户端与服务器之间使用TCP协议进行通信,并且很早就支持管道技术了。在某些高并发的场景下,网络开销成了Redis速度的瓶颈,所以需要使用管道技术来实现突破。
在介绍管道之前,先来想一下单条命令的执行步骤:
1)客户端把命令发送到服务器,然后阻塞客户端,等待着从socket读取服务器的返回结果。
2) 服务器处理命令并将结果返回给客户端。
按照这样的描述,每个命令的执行时间 = 客户端发送时间+服务器处理和返回时间+一个网络来回的时间
其中一个网络来回的时间是不固定的,它的决定因素有很多,比如客户端到服务器要经过多少跳,网络是否拥堵等等。但是这个时间的量级也是最大的,也就是说一个命令的完成时间的长度很大程度上取决于网络开销。如果我们的服务器每秒可以处理10万条请求,而网络开销是250毫秒,那么实际上每秒钟只能处理4个请求。最暴力的优化方法就是使客户端和服务器在一台物理机上,这样就可以将网络开销降低到1ms以下。但是实际的生产环境我们并不会这样做。而且即使使用这种方法,当请求非常频繁时,这个时间和服务器处理时间比较仍然是很长的。
为了解决这种问题,Redis在很早就支持了管道技术。也就是说客户端可以一次发送多条命令,不用逐条等待命令的返回值,而是到最后一起读取返回结果,这样只需要一次网络开销,速度就会得到明显的提升。管道技术其实已经非常成熟并且得到广泛应用了,例如POP3协议由于支持管道技术,从而显著提高了从服务器下载邮件的速度。
下面展示普通的插入数据和使用管道插入数据的对比:
public void testPipeLine(){
long time1 = System.currentTimeMillis();
/* 插入多条数据 */
for(Integer i = 0; i < 100000; i++) {
jedis.set(i.toString(), i.toString());
}
for(Integer i = 0; i < 100000; i++) {
jedis.del(i.toString());
}
long time2 = System.currentTimeMillis();
System.out.println(time2 - time1);
Pipeline pipelined = jedis.pipelined();
long time3 = System.currentTimeMillis();
for(Integer i = 0; i < 100000; i++) {
pipelined.set(i.toString(), i.toString());
}
for(Integer i = 0; i < 100000; i++) {
pipelined.del(i.toString());
}
pipelined.sync();
long time4 = System.currentTimeMillis();
System.out.println(time4 - time3);
}
执行结果如下:
/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/bin/java...
35204
579
Process finished with exit code 0
可以看到使用普通的插入和删除10万条数据花了大概35秒的时间,而使用管道pipeline技术花了大概0.5秒左右的时间,可想而知这之间的差距。
管道pipeline的局限性:
1)鉴于Pipepining发送命令的特性,Redis服务器是以队列来存储准备执行的命令,而队列是存放在有限的内存中的,所以不宜一次性发送过多的命令。如果需要大量的命令,可分批进行,效率不会相差太远的,总好过内存溢出。
2)由于pipeline的原理是收集需执行的命令,到最后才一次性执行。所以无法在中途立即查得数据的结果(需待pipelining完毕后才能查得结果),这样会使得无法立即查得数据进行条件判断(比如判断是非继续插入记录)。